diff options
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/extras/chrome')
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> |