summaryrefslogtreecommitdiffstats
path: root/chromium/third_party/catapult/tracing/tracing/extras/chrome
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/extras/chrome')
-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
57 files changed, 9452 insertions, 0 deletions
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>