diff options
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/ui')
410 files changed, 73569 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view.html new file mode 100644 index 00000000000..b44741aace1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view.html @@ -0,0 +1,181 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<dom-module id='tr-ui-a-alert-sub-view'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + #table { + flex: 1 1 auto; + align-self: stretch; + font-size: 12px; + } + </style> + <tr-ui-b-table id="table"> + </tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-alert-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + ready() { + this.currentSelection_ = undefined; + this.$.table.tableColumns = [ + { + title: 'Label', + value(row) { return row.name; }, + width: '150px' + }, + { + title: 'Value', + width: '100%', + value(row) { return row.value; } + } + ]; + this.$.table.showHeader = false; + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + this.currentSelection_ = selection; + this.updateContents_(); + }, + + getRowsForSingleAlert_(alert) { + const rows = []; + + // Arguments + for (const argName in alert.args) { + const argView = + document.createElement('tr-ui-a-generic-object-view'); + argView.object = alert.args[argName]; + rows.push({ name: argName, value: argView }); + } + + // Associated events + if (alert.associatedEvents.length) { + alert.associatedEvents.forEach(function(event, i) { + const linkEl = document.createElement('tr-ui-a-analysis-link'); + linkEl.setSelectionAndContent( + new tr.model.EventSet(event), event.title); + + let valueString = ''; + if (event instanceof tr.model.TimedEvent) { + valueString = 'took ' + event.duration.toFixed(2) + 'ms'; + } + + rows.push({ + name: linkEl, + value: valueString + }); + }); + } + + // Description + const descriptionEl = tr.ui.b.createDiv({ + textContent: alert.info.description, + maxWidth: '300px' + }); + rows.push({ + name: 'Description', + value: descriptionEl + }); + + // Additional Reading Links + if (alert.info.docLinks) { + alert.info.docLinks.forEach(function(linkObject) { + const linkEl = document.createElement('a'); + linkEl.target = '_blank'; + linkEl.href = linkObject.href; + Polymer.dom(linkEl).textContent = Polymer.dom(linkObject).textContent; + rows.push({ + name: linkObject.label, + value: linkEl + }); + }); + } + return rows; + }, + + getRowsForAlerts_(alerts) { + if (alerts.length === 1) { + const rows = [{ + name: 'Alert', + value: tr.b.getOnlyElement(alerts).title + }]; + const detailRows = this.getRowsForSingleAlert_(tr.b.getOnlyElement( + alerts)); + rows.push.apply(rows, detailRows); + return rows; + } + return alerts.map(function(alert) { + return { + name: 'Alert', + value: alert.title, + isExpanded: alerts.size < 10, // This is somewhat arbitrary for now. + subRows: this.getRowsForSingleAlert_(alert) + }; + }, this); + }, + + updateContents_() { + if (this.currentSelection_ === undefined) { + this.$.table.rows = []; + this.$.table.rebuild(); + return; + } + + const alerts = this.currentSelection_; + this.$.table.tableRows = this.getRowsForAlerts_(alerts); + this.$.table.rebuild(); + }, + + get relatedEventsToHighlight() { + if (!this.currentSelection_) return undefined; + const result = new tr.model.EventSet(); + for (const event of this.currentSelection_) { + result.addEventSet(event.associatedEvents); + } + return result; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-alert-sub-view', + tr.model.Alert, + { + multi: false, + title: 'Alert', + }); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-alert-sub-view', + tr.model.Alert, + { + multi: true, + title: 'Alerts', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view_test.html new file mode 100644 index 00000000000..574cf5f0b86 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view_test.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_view.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + + test('instantiate', function() { + const slice = newSliceEx({title: 'b', start: 0, duration: 0.002}); + + const alertInfo = new tr.model.EventInfo( + 'alertInfo', 'Critical alert', + [{ + label: 'Project Page', + textContent: 'Trace-Viewer Github Project', + href: 'https://github.com/google/trace-viewer/' + }]); + + const alert = new tr.model.Alert(alertInfo, 5, [slice]); + assert.strictEqual(1, alert.associatedEvents.length); + + const subView = document.createElement('tr-ui-a-alert-sub-view'); + subView.selection = new tr.model.EventSet(alert); + assert.isTrue( + subView.relatedEventsToHighlight.equals(alert.associatedEvents)); + this.addHTMLOutput(subView); + + const table = tr.ui.b.findDeepElementMatching( + subView, 'tr-ui-b-table'); + + const rows = table.tableRows; + const columns = table.tableColumns; + assert.lengthOf(rows, 4); + assert.lengthOf(columns, 2); + }); + + test('instantiate_twoAlertsWithRelatedEvents', function() { + const slice1 = newSliceEx({title: 'b', start: 0, duration: 0.002}); + const slice2 = newSliceEx({title: 'b', start: 1, duration: 0.002}); + + const alertInfo1 = new tr.model.EventInfo( + 'alertInfo1', 'Critical alert', + [{ + label: 'Project Page', + textContent: 'Trace-Viewer Github Project', + href: 'https://github.com/google/trace-viewer/' + }]); + + const alertInfo2 = new tr.model.EventInfo( + 'alertInfo2', 'Critical alert', + [{ + label: 'Google Homepage', + textContent: 'Google Search Page', + href: 'http://www.google.com' + }]); + + const alert1 = new tr.model.Alert(alertInfo1, 5, [slice1]); + const alert2 = new tr.model.Alert(alertInfo2, 5, [slice2]); + + const subView = document.createElement('tr-ui-a-alert-sub-view'); + subView.selection = new tr.model.EventSet([alert1, alert2]); + assert.isTrue(subView.relatedEventsToHighlight.equals( + new tr.model.EventSet([ + tr.b.getOnlyElement(alert1.associatedEvents), + tr.b.getOnlyElement(alert2.associatedEvents) + ]))); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link.html new file mode 100644 index 00000000000..8d996afeeb9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link.html @@ -0,0 +1,147 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/brushing_state_controller.html"> + +<dom-module id='tr-ui-a-analysis-link'> + <template> + <style> + :host { + display: inline; + cursor: pointer; + cursor: pointer; + white-space: nowrap; + } + a { + text-decoration: underline; + } + </style> + <a href="{{href}}" on-click="onClicked_" on-mouseenter="onMouseEnter_" on-mouseleave="onMouseLeave_"><slot></slot></a> + + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-analysis-link', + + properties: { + href: { + type: String + } + }, + + listeners: { + 'click': 'onClicked_', + 'mouseenter': 'onMouseEnter_', + 'mouseleave': 'onMouseLeave_' + }, + + ready() { + this.selection_ = undefined; + }, + + attached() { + // Save an instance of the controller since it's going to be used in + // detached() where it can no longer be obtained. + this.controller_ = + tr.c.BrushingStateController.getControllerForElement(this); + }, + + detached() { + // Reset highlights. + this.clearHighlight_(); + this.controller_ = undefined; + }, + + set color(c) { + this.style.color = c; + }, + + /** + * @return {*|function():*} + */ + get selection() { + return this.selection_; + }, + + /** + * |selection| can be anything except a function, or else a function that + * can return anything. + * + * In the context of trace_viewer, |selection| is typically an EventSet, + * whose events will be highlighted by trace_viewer when this link is + * clicked or mouse-entered. + * + * If |selection| is not a function, then it will be dispatched to this + * link's embedder via a RequestSelectionChangeEvent when this link is + * clicked or mouse-entered. + * + * If |selection| is a function, then it will be called when this link is + * clicked or mouse-entered, and its result will be dispatched to this + * link's embedder via a RequestSelectionChangeEvent. + * + * @param {*|function():*} selection + */ + set selection(selection) { + this.selection_ = selection; + Polymer.dom(this).textContent = selection.userFriendlyName; + }, + + setSelectionAndContent(selection, opt_textContent) { + this.selection_ = selection; + if (opt_textContent) { + Polymer.dom(this).textContent = opt_textContent; + } + }, + + /** + * If |selection| is a function, call it and return the result. + * Otherwise return |selection| directly. + * + * @return {*} + */ + getCurrentSelection_() { + // Gets the current selection, invoking the selection function if needed. + if (typeof this.selection_ === 'function') { + return this.selection_(); + } + return this.selection_; + }, + + setHighlight_(opt_eventSet) { + if (this.controller_) { + this.controller_.changeAnalysisLinkHoveredEvents(opt_eventSet); + } + }, + + clearHighlight_(opt_eventSet) { + this.setHighlight_(); + }, + + onClicked_(clickEvent) { + if (!this.selection_) return; + + clickEvent.stopPropagation(); + + const event = new tr.model.RequestSelectionChangeEvent(); + event.selection = this.getCurrentSelection_(); + this.dispatchEvent(event); + }, + + onMouseEnter_() { + this.setHighlight_(this.getCurrentSelection_()); + }, + + onMouseLeave_() { + this.clearHighlight_(); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link_test.html new file mode 100644 index 00000000000..e8caed8f601 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link_test.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('testBasic', function() { + const link = document.createElement('tr-ui-a-analysis-link'); + + const i10 = new tr.model.ObjectInstance( + {}, '0x1000', 'cat', 'name', 10); + const s10 = i10.addSnapshot(10, {foo: 1}); + + link.selection = new tr.model.EventSet(s10); + this.addHTMLOutput(link); + + let didRSC = false; + link.addEventListener('requestSelectionChange', function(e) { + didRSC = true; + assert.isTrue(e.selection.equals(new tr.model.EventSet(s10))); + }); + link.click(); + assert.isTrue(didRSC); + }); + + test('testGeneratorVersion', function() { + const link = document.createElement('tr-ui-a-analysis-link'); + + const i10 = new tr.model.ObjectInstance( + {}, '0x1000', 'cat', 'name', 10); + const s10 = i10.addSnapshot(10, {foo: 1}); + + function selectionGenerator() { + return new tr.model.EventSet(s10); + } + selectionGenerator.userFriendlyName = 'hello world'; + link.selection = selectionGenerator; + this.addHTMLOutput(link); + + let didRSC = false; + link.addEventListener('requestSelectionChange', function(e) { + assert.isTrue(e.selection.equals(new tr.model.EventSet(s10))); + didRSC = true; + }); + link.click(); + assert.isTrue(didRSC); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view.html new file mode 100644 index 00000000000..8bd967c8c75 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view.html @@ -0,0 +1,266 @@ +<!DOCTYPE html> +<!-- +Copyright 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> + +<!-- +@fileoverview Polymer element for various analysis sub-views. +--> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const AnalysisSubView = { + set tabLabel(label) { + Polymer.dom(this).setAttribute('tab-label', label); + }, + + get tabLabel() { + return this.getAttribute('tab-label'); + }, + + get requiresTallView() { + return false; + }, + + get relatedEventsToHighlight() { + return undefined; + }, + + /** + * Each element extending this one must implement + * a 'selection' property. + */ + set selection(selection) { + throw new Error('Not implemented!'); + }, + + get selection() { + throw new Error('Not implemented!'); + } + }; + + // Basic registry. + const allTypeInfosByEventProto = new Map(); + let onlyRootTypeInfosByEventProto = undefined; + let eventProtoToRootTypeInfoMap = undefined; + + function AnalysisSubViewTypeInfo(eventConstructor, options) { + if (options.multi === undefined) { + throw new Error('missing field: multi'); + } + if (options.title === undefined) { + throw new Error('missing field: title'); + } + this.eventConstructor = eventConstructor; + + this.singleTagName = undefined; + this.singleTitle = undefined; + + this.multiTagName = undefined; + this.multiTitle = undefined; + + // This is computed by rebuildRootSubViewTypeInfos, so don't muck with it! + this.childrenTypeInfos_ = undefined; + } + + AnalysisSubViewTypeInfo.prototype = { + get childrenTypeInfos() { + return this.childrenTypeInfos_; + }, + + resetchildrenTypeInfos() { + this.childrenTypeInfos_ = []; + } + }; + + AnalysisSubView.register = function(tagName, eventConstructor, options) { + let typeInfo = allTypeInfosByEventProto.get(eventConstructor.prototype); + if (typeInfo === undefined) { + typeInfo = new AnalysisSubViewTypeInfo(eventConstructor, options); + allTypeInfosByEventProto.set(typeInfo.eventConstructor.prototype, + typeInfo); + + onlyRootTypeInfosByEventProto = undefined; + } + + if (!options.multi) { + if (typeInfo.singleTagName !== undefined) { + throw new Error('SingleTagName already set'); + } + typeInfo.singleTagName = tagName; + typeInfo.singleTitle = options.title; + } else { + if (typeInfo.multiTagName !== undefined) { + throw new Error('MultiTagName already set'); + } + typeInfo.multiTagName = tagName; + typeInfo.multiTitle = options.title; + } + return typeInfo; + }; + + function rebuildRootSubViewTypeInfos() { + onlyRootTypeInfosByEventProto = new Map(); + allTypeInfosByEventProto.forEach(function(typeInfo) { + typeInfo.resetchildrenTypeInfos(); + }); + + // Find all root typeInfos. + allTypeInfosByEventProto.forEach(function(typeInfo, eventProto) { + const eventPrototype = typeInfo.eventConstructor.prototype; + + let lastEventProto = eventPrototype; + let curEventProto = eventPrototype.__proto__; + while (true) { + if (!allTypeInfosByEventProto.has(curEventProto)) { + const rootTypeInfo = allTypeInfosByEventProto.get(lastEventProto); + const rootEventProto = lastEventProto; + + const isNew = onlyRootTypeInfosByEventProto.has(rootEventProto); + onlyRootTypeInfosByEventProto.set(rootEventProto, + rootTypeInfo); + break; + } + + lastEventProto = curEventProto; + curEventProto = curEventProto.__proto__; + } + }); + + // Build the childrenTypeInfos array. + allTypeInfosByEventProto.forEach(function(typeInfo, eventProto) { + const eventPrototype = typeInfo.eventConstructor.prototype; + const parentEventProto = eventPrototype.__proto__; + const parentTypeInfo = allTypeInfosByEventProto.get(parentEventProto); + if (!parentTypeInfo) return; + parentTypeInfo.childrenTypeInfos.push(typeInfo); + }); + + // Build the eventProto to rootTypeInfo map. + eventProtoToRootTypeInfoMap = new Map(); + allTypeInfosByEventProto.forEach(function(typeInfo, eventProto) { + const eventPrototype = typeInfo.eventConstructor.prototype; + + let curEventProto = eventPrototype; + while (true) { + if (onlyRootTypeInfosByEventProto.has(curEventProto)) { + const rootTypeInfo = onlyRootTypeInfosByEventProto.get( + curEventProto); + eventProtoToRootTypeInfoMap.set(eventPrototype, + rootTypeInfo); + break; + } + curEventProto = curEventProto.__proto__; + } + }); + } + + function findLowestTypeInfoForEvents(thisTypeInfo, events) { + if (events.length === 0) return thisTypeInfo; + const event0 = tr.b.getFirstElement(events); + + let candidateSubTypeInfo; + for (let i = 0; i < thisTypeInfo.childrenTypeInfos.length; i++) { + const childTypeInfo = thisTypeInfo.childrenTypeInfos[i]; + if (event0 instanceof childTypeInfo.eventConstructor) { + candidateSubTypeInfo = childTypeInfo; + break; + } + } + if (!candidateSubTypeInfo) return thisTypeInfo; + + // Validate that all the other events are instances of the candidate type. + let allMatch = true; + for (const event of events) { + if (event instanceof candidateSubTypeInfo.eventConstructor) continue; + allMatch = false; + break; + } + + if (!allMatch) { + return thisTypeInfo; + } + + return findLowestTypeInfoForEvents(candidateSubTypeInfo, events); + } + + const primaryEventProtoToTypeInfoMap = new Map(); + function getRootTypeInfoForEvent(event) { + const curProto = event.__proto__; + const typeInfo = primaryEventProtoToTypeInfoMap.get(curProto); + if (typeInfo) return typeInfo; + return getRootTypeInfoForEventSlow(event); + } + + function getRootTypeInfoForEventSlow(event) { + let typeInfo; + let curProto = event.__proto__; + while (true) { + if (curProto === Object.prototype) { + throw new Error('No view registered for ' + event.toString()); + } + typeInfo = onlyRootTypeInfosByEventProto.get(curProto); + if (typeInfo) { + primaryEventProtoToTypeInfoMap.set(event.__proto__, typeInfo); + return typeInfo; + } + curProto = curProto.__proto__; + } + } + + AnalysisSubView.getEventsOrganizedByTypeInfo = function(selection) { + if (onlyRootTypeInfosByEventProto === undefined) { + rebuildRootSubViewTypeInfos(); + } + + // Base grouping. + const eventsByRootTypeInfo = tr.b.groupIntoMap( + selection, + function(event) { + return getRootTypeInfoForEvent(event); + }, + this, tr.model.EventSet); + + // Now, try to lower the typeinfo to the most specific type that still + // encompasses the event group. + // + // For instance, if we have 3 ThreadSlices, and all three are V8 slices, + // then we can convert this to use the V8Slices's typeinfos. But, if one + // of those slices was not a V8Slice, then we must still use + // ThreadSlice. + // + // The reason for this is for the confusion that might arise from the + // alternative. Suppose you click on a set of mixed slices, we want to show + // you the most correct information, and let you navigate to . If we instead + // showed you a V8 slices tab, and a Slices tab, we present the user with an + // ambiguity: is the V8 slice also in the Slices tab? Or is it not? Better, + // we think, to just only ever show an event in one place at a time, and + // avoid the possible confusion. + const eventsByLowestTypeInfo = new Map(); + eventsByRootTypeInfo.forEach(function(events, typeInfo) { + const lowestTypeInfo = findLowestTypeInfoForEvents(typeInfo, events); + eventsByLowestTypeInfo.set(lowestTypeInfo, events); + }); + + return eventsByLowestTypeInfo; + }; + + return { + AnalysisSubView, + AnalysisSubViewTypeInfo, + }; +}); + +// Dummy element for testing +Polymer({ + is: 'tr-ui-a-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView] +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view_test.html new file mode 100644 index 00000000000..0f3e85ea4ec --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view_test.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('subViewThrowsNotImplementedErrors', function() { + const subView = document.createElement('tr-ui-a-sub-view'); + + assert.throw(function() { + subView.selection = new tr.model.EventSet(); + }, 'Not implemented!'); + + assert.throw(function() { + const viewSelection = subView.selection; + }, 'Not implemented!'); + + subView.tabLabel = 'Tab Label'; + assert.strictEqual(subView.getAttribute('tab-label'), 'Tab Label'); + assert.strictEqual(subView.tabLabel, 'Tab Label'); + + subView.tabLabel = 'New Label'; + assert.strictEqual(subView.getAttribute('tab-label'), 'New Label'); + assert.strictEqual(subView.tabLabel, 'New Label'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view.html new file mode 100644 index 00000000000..edc14edca11 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view.html @@ -0,0 +1,207 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/alert_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/container_memory_dump_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/counter_sample_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_async_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_cpu_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_flow_event_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_frame_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/multi_instant_event_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_object_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_power_sample_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_sample_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_thread_slice_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/multi_thread_time_slice_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/multi_user_expectation_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/single_async_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/single_cpu_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/single_flow_event_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/single_frame_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/single_instant_event_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/single_object_instance_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/single_object_snapshot_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/single_power_sample_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/single_sample_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/single_thread_slice_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/single_thread_time_slice_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/single_user_expectation_sub_view.html"> +<link rel="import" href="/tracing/ui/base/tab_view.html"> + +<!-- +@fileoverview A component used to display an analysis of a selection, +using custom elements specialized for different event types. +--> +<dom-module id='tr-ui-a-analysis-view'> + <template> + <style> + :host { + background-color: white; + display: flex; + flex-direction: column; + height: 275px; + overflow: auto; + } + + :host(.tall-mode) { + height: 525px; + } + </style> + <slot></slot> + </template> +</dom-module> +<script> +'use strict'; +(function() { + const EventRegistry = tr.model.EventRegistry; + + /** Returns the label that goes next to the list of tabs. */ + function getTabStripLabel(numEvents) { + if (numEvents === 0) { + return 'Nothing selected. Tap stuff.'; + } else if (numEvents === 1) { + return '1 item selected.'; + } + return numEvents + ' items selected.'; + } + + function createSubView(subViewTypeInfo, selection) { + let tagName; + if (selection.length === 1) { + tagName = subViewTypeInfo.singleTagName; + } else { + tagName = subViewTypeInfo.multiTagName; + } + + if (tagName === undefined) { + throw new Error('No view registered for ' + + subViewTypeInfo.eventConstructor.name); + } + const subView = document.createElement(tagName); + + let title; + if (selection.length === 1) { + title = subViewTypeInfo.singleTitle; + } else { + title = subViewTypeInfo.multiTitle; + } + title += ' (' + selection.length + ')'; + subView.tabLabel = title; + + subView.selection = selection; + return subView; + } + + Polymer({ + is: 'tr-ui-a-analysis-view', + + ready() { + this.brushingStateController_ = undefined; + this.lastSelection_ = undefined; + this.tabView_ = document.createElement('tr-ui-b-tab-view'); + this.tabView_.addEventListener( + 'selected-tab-change', this.onSelectedSubViewChanged_.bind(this)); + + Polymer.dom(this).appendChild(this.tabView_); + }, + + set tallMode(value) { + Polymer.dom(this).classList.toggle('tall-mode', value); + }, + + get tallMode() { + return Polymer.dom(this).classList.contains('tall-mode'); + }, + + get tabView() { + return this.tabView_; + }, + + get brushingStateController() { + return this.brushingStateController_; + }, + + set brushingStateController(brushingStateController) { + if (this.brushingStateController_) { + this.brushingStateController_.removeEventListener( + 'change', this.onSelectionChanged_.bind(this)); + } + + this.brushingStateController_ = brushingStateController; + if (this.brushingStateController) { + this.brushingStateController_.addEventListener( + 'change', this.onSelectionChanged_.bind(this)); + } + + // The new brushing controller may have a different selection than the + // last one, so we have to refresh the subview. + this.onSelectionChanged_(); + }, + + get selection() { + return this.brushingStateController_.selection; + }, + + onSelectionChanged_(e) { + if (this.lastSelection_ && this.selection.equals(this.lastSelection_)) { + return; + } + this.lastSelection_ = this.selection; + + this.tallMode = false; + + this.tabView_.label = getTabStripLabel(this.selection.length); + const eventsByBaseTypeName = + this.selection.getEventsOrganizedByBaseType(true); + + const ASV = tr.ui.analysis.AnalysisSubView; + const eventsByTagName = ASV.getEventsOrganizedByTypeInfo(this.selection); + const newSubViews = []; + eventsByTagName.forEach(function(events, typeInfo) { + newSubViews.push(createSubView(typeInfo, events)); + }); + + this.tabView_.resetSubViews(newSubViews); + }, + + onSelectedSubViewChanged_() { + const selectedSubView = this.tabView_.selectedSubView; + + if (!selectedSubView) { + this.tallMode = false; + this.maybeChangeRelatedEvents_(undefined); + return; + } + + this.tallMode = selectedSubView.requiresTallView; + this.maybeChangeRelatedEvents_(selectedSubView.relatedEventsToHighlight); + }, + + /** Changes the highlighted related events if possible. */ + maybeChangeRelatedEvents_(events) { + if (this.brushingStateController) { + this.brushingStateController.changeAnalysisViewRelatedEvents(events); + } + } + }); +})(); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view_test.html new file mode 100644 index 00000000000..fa7b51256a6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view_test.html @@ -0,0 +1,141 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/counter.html"> +<link rel="import" href="/tracing/model/counter_sample.html"> +<link rel="import" href="/tracing/model/counter_series.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/user_model/stub_expectation.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_view.html"> +<link rel="import" href="/tracing/ui/brushing_state_controller.html"> +<link rel="import" href="/tracing/ui/extras/full_config.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const EventSet = tr.model.EventSet; + const BrushingStateController = tr.c.BrushingStateController; + const Model = tr.Model; + const Counter = tr.model.Counter; + const CounterSeries = tr.model.CounterSeries; + const CounterSample = tr.model.CounterSample; + const newThreadSlice = tr.c.TestUtils.newThreadSlice; + const SCHEDULING_STATE = tr.model.SCHEDULING_STATE; + const StubExpectation = tr.model.um.StubExpectation; + + function assertEventSet(actualEventSet, expectedEvents) { + const expectedEventSet = new EventSet(expectedEvents); + assert.isTrue(actualEventSet.equals(expectedEventSet), + 'EventSet objects are not equal'); + } + + function checkTab(tab, expectedTagName, expectedSelectionEvents) { + assert.strictEqual(tab.tagName, expectedTagName.toUpperCase()); + assertEventSet(tab.selection, expectedSelectionEvents); + } + + test('selectedTabChange', function() { + // Set up the model. + const model = new Model(); + const process = model.getOrCreateProcess(1); + + const counter = process.getOrCreateCounter('universe', 'planets'); + const series = counter.addSeries(new CounterSeries('x', 0)); + const sample1 = series.addCounterSample(0, 100); + const sample2 = series.addCounterSample(1, 90); + const sample3 = series.addCounterSample(2, 80); + + const thread = process.getOrCreateThread(2); + const slice1 = newThreadSlice(thread, SCHEDULING_STATE.RUNNING, 0, 1); + const slice2 = newThreadSlice(thread, SCHEDULING_STATE.SLEEPING, 1, 2.718); + thread.timeSlices = [slice1, slice2]; + + const record1 = new StubExpectation( + {parentModel: model, initiatorTitle: 'r1', start: 200, duration: 300}); + record1.associatedEvents.push(sample1); + record1.associatedEvents.push(slice1); + const record2 = new StubExpectation( + {parentModel: model, initiatorTitle: 'r2', start: 600, duration: 100}); + record2.associatedEvents.push(sample2); + record2.associatedEvents.push(sample3); + record2.associatedEvents.push(slice1); + + // Set up the analysis views and brushing state controller. + const analysisView = document.createElement('tr-ui-a-analysis-view'); + this.addHTMLOutput(analysisView); + const tabView = analysisView.tabView; + const controller = new BrushingStateController(undefined); + analysisView.brushingStateController = controller; + + function checkSelectedTab(expectedSelectedTab, expectedRelatedEvents) { + assert.strictEqual(tabView.selectedSubView, expectedSelectedTab); + assertEventSet(controller.currentBrushingState.analysisViewRelatedEvents, + expectedRelatedEvents); + } + + // 1. Empty selection (implicit). + assert.lengthOf(tabView.tabs, 0); + checkSelectedTab(undefined, []); + + // 2. Event selection: two samples and one thread slice. + controller.changeSelectionFromRequestSelectionChangeEvent( + new EventSet([sample1, slice1, sample2])); + assert.lengthOf(tabView.tabs, 2); + const sampleTab2 = tabView.tabs[0]; + checkTab(sampleTab2, + 'tr-ui-a-counter-sample-sub-view', + [sample1, sample2]); + const singleThreadSliceTab2 = tabView.tabs[1]; + checkTab(singleThreadSliceTab2, + 'tr-ui-a-single-thread-time-slice-sub-view', + [slice1]); + // First tab should be selected. + checkSelectedTab(sampleTab2, []); + + // 3. Tab selection: single thread slice tab. + tabView.selectedSubView = singleThreadSliceTab2; + checkSelectedTab(singleThreadSliceTab2, []); + + // 4. Event selection: one sample, two thread slices, and one + // user expectation. + controller.changeSelectionFromRequestSelectionChangeEvent( + new EventSet([slice1, slice2, sample3, record1])); + assert.lengthOf(tabView.tabs, 3); + const sampleTab4 = tabView.tabs[1]; + checkTab(sampleTab4, + 'tr-ui-a-counter-sample-sub-view', + [sample3]); + const singleRecordTab4 = tabView.tabs[2]; + checkTab(singleRecordTab4, + 'tr-ui-a-single-user-expectation-sub-view', + [record1]); + const multiThreadSliceTab4 = tabView.tabs[0]; + checkTab(multiThreadSliceTab4, + 'tr-ui-a-multi-thread-time-slice-sub-view', + [slice1, slice2]); + // Remember selected tab (even though the tab was destroyed). + checkSelectedTab(multiThreadSliceTab4, []); + + // 5. Tab selection: single user expectation tab. + tabView.selectedSubView = singleRecordTab4; + checkSelectedTab(singleRecordTab4, [sample1, slice1]); + + // 6. Event selection: one user expectation. + controller.changeSelectionFromRequestSelectionChangeEvent( + new EventSet([record2])); + assert.lengthOf(tabView.tabs, 1); + const singleRecordTab6 = tabView.tabs[0]; + checkTab(singleRecordTab6, + 'tr-ui-a-single-user-expectation-sub-view', + [record2]); + // Remember selected tab. + checkSelectedTab(singleRecordTab6, [sample2, sample3, slice1]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view.html new file mode 100644 index 00000000000..cc0e9155358 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view.html @@ -0,0 +1,200 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_header_pane.html"> +<link rel="import" href="/tracing/ui/analysis/stacked_pane_view.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-container-memory-dump-sub-view'> + <template> + <style> + tr-ui-b-table { + font-size: 12px; + } + </style> + <div id="content"></div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + Polymer({ + is: 'tr-ui-a-container-memory-dump-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + set selection(selection) { + if (selection === undefined) { + this.currentSelection_ = undefined; + this.dumpsByContainerName_ = undefined; + this.updateContents_(); + return; + } + + // Check that the selection contains only container memory dumps. + selection.forEach(function(event) { + if (!(event instanceof tr.model.ContainerMemoryDump)) { + throw new Error( + 'Memory dump sub-view only supports container memory dumps'); + } + }); + this.currentSelection_ = selection; + + // Group the selected memory dumps by container name and sort them + // chronologically. + this.dumpsByContainerName_ = tr.b.groupIntoMap( + this.currentSelection_.toArray(), dump => dump.containerName); + for (const dumps of this.dumpsByContainerName_.values()) { + dumps.sort((a, b) => a.start - b.start); + } + + this.updateContents_(); + }, + + get selection() { + return this.currentSelection_; + }, + + get requiresTallView() { + return true; + }, + + updateContents_() { + Polymer.dom(this.$.content).textContent = ''; + + if (this.dumpsByContainerName_ === undefined) return; + + const containerNames = Array.from(this.dumpsByContainerName_.keys()); + if (containerNames.length === 0) return; + + if (containerNames.length > 1) { + this.buildViewForMultipleContainerNames_(); + } else { + this.buildViewForSingleContainerName_(); + } + }, + + buildViewForSingleContainerName_() { + const containerMemoryDumps = tr.b.getFirstElement( + this.dumpsByContainerName_.values()); + const dumpView = this.ownerDocument.createElement( + 'tr-ui-a-stacked-pane-view'); + Polymer.dom(this.$.content).appendChild(dumpView); + dumpView.setPaneBuilder(function() { + const headerPane = document.createElement( + 'tr-ui-a-memory-dump-header-pane'); + headerPane.containerMemoryDumps = containerMemoryDumps; + return headerPane; + }); + }, + + buildViewForMultipleContainerNames_() { + // TODO(petrcermak): Provide a more sophisticated view for this case. + const ownerDocument = this.ownerDocument; + + const rows = []; + for (const [containerName, dumps] of this.dumpsByContainerName_) { + rows.push({ + containerName, + subRows: dumps, + isExpanded: true, + }); + } + rows.sort(function(a, b) { + return a.containerName.localeCompare(b.containerName); + }); + + const columns = [ + { + title: 'Dump', + + value(row) { + if (row.subRows === undefined) { + return this.singleDumpValue_(row); + } + return this.groupedDumpValue_(row); + }, + + singleDumpValue_(row) { + const linkEl = ownerDocument.createElement('tr-ui-a-analysis-link'); + linkEl.setSelectionAndContent(new tr.model.EventSet([row])); + Polymer.dom(linkEl).appendChild(tr.v.ui.createScalarSpan( + row.start, { + unit: tr.b.Unit.byName.timeStampInMs, + ownerDocument + })); + return linkEl; + }, + + groupedDumpValue_(row) { + const linkEl = ownerDocument.createElement('tr-ui-a-analysis-link'); + linkEl.setSelectionAndContent(new tr.model.EventSet(row.subRows)); + Polymer.dom(linkEl).appendChild(tr.ui.b.createSpan({ + ownerDocument, + textContent: row.subRows.length + ' memory dump' + + (row.subRows.length === 1 ? '' : 's') + ' in ' + })); + Polymer.dom(linkEl).appendChild(tr.ui.b.createSpan({ + ownerDocument, + textContent: row.containerName, + bold: true + })); + return linkEl; + } + } + ]; + + const table = this.ownerDocument.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = rows; + table.showHeader = false; + table.rebuild(); + Polymer.dom(this.$.content).appendChild(table); + } + }); + + tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-container-memory-dump-sub-view', + tr.model.GlobalMemoryDump, + { + multi: false, + title: 'Global Memory Dump', + }); + + tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-container-memory-dump-sub-view', + tr.model.GlobalMemoryDump, + { + multi: true, + title: 'Global Memory Dumps', + }); + + tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-container-memory-dump-sub-view', + tr.model.ProcessMemoryDump, + { + multi: false, + title: 'Process Memory Dump', + }); + + tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-container-memory-dump-sub-view', + tr.model.ProcessMemoryDump, + { + multi: true, + title: 'Process Memory Dumps', + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view_test.html new file mode 100644 index 00000000000..974837f545e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view_test.html @@ -0,0 +1,351 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" + href="/tracing/ui/analysis/container_memory_dump_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/ui/brushing_state_controller.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const EventSet = tr.model.EventSet; + const extractVmRegions = tr.ui.analysis.extractVmRegions; + const extractMemoryAllocatorDumps = + tr.ui.analysis.extractMemoryAllocatorDumps; + const extractHeapDumps = tr.ui.analysis.extractHeapDumps; + + function createViewWithSelection(selection, opt_parentElement) { + const viewEl = document.createElement( + 'tr-ui-a-container-memory-dump-sub-view'); + if (opt_parentElement) { + Polymer.dom(opt_parentElement).appendChild(viewEl); + } + if (selection === undefined) { + viewEl.selection = undefined; + } else { + // Rotate the list of selected dumps to check that the sub-view sorts + // them properly. + const length = selection.length; + viewEl.selection = new tr.model.EventSet( + selection.slice(length / 2, length).concat( + selection.slice(0, length / 2))); + } + return viewEl; + } + + function createAndCheckContainerMemoryDumpView( + test, containerMemoryDumps, detailsCheckCallback, opt_parentElement) { + const viewEl = + createViewWithSelection(containerMemoryDumps, opt_parentElement); + if (!opt_parentElement) { + test.addHTMLOutput(viewEl); + } + + // The view should contain a stacked pane view with memory dump header and + // overview panes. + const stackedPaneViewEl = tr.ui.b.findDeepElementMatching( + viewEl, 'tr-ui-a-stacked-pane-view'); + const headerPaneEl = tr.ui.b.findDeepElementMatching( + stackedPaneViewEl, 'tr-ui-a-memory-dump-header-pane'); + const overviewPaneEl = tr.ui.b.findDeepElementMatching( + stackedPaneViewEl, 'tr-ui-a-memory-dump-overview-pane'); + + // Check that the header pane and overview pane are correctly set up. + const processMemoryDumps = containerMemoryDumps.map( + containerDump => containerDump.processMemoryDumps); + assert.deepEqual( + Array.from(headerPaneEl.containerMemoryDumps), containerMemoryDumps); + assert.deepEqual(overviewPaneEl.processMemoryDumps, processMemoryDumps); + assert.strictEqual( + overviewPaneEl.aggregationMode, headerPaneEl.aggregationMode); + + // Get the overview pane table to drive the details pane checks. + const overviewTableEl = tr.ui.b.findDeepElementMatching( + overviewPaneEl, 'tr-ui-b-table'); + + function checkVmRegionsPane(pid) { + const detailsPaneEl = tr.ui.b.findDeepElementMatching( + stackedPaneViewEl, 'tr-ui-a-memory-dump-vm-regions-details-pane'); + if (pid === undefined) { + assert.isUndefined(detailsPaneEl); + } else { + assert.deepEqual(Array.from(detailsPaneEl.vmRegions), + extractVmRegions(processMemoryDumps, pid)); + assert.strictEqual( + detailsPaneEl.aggregationMode, headerPaneEl.aggregationMode); + } + } + + function checkAllocatorPane(pid, allocatorName, withHeapDetailsPane) { + const allocatorDetailsPaneEl = tr.ui.b.findDeepElementMatching( + stackedPaneViewEl, 'tr-ui-a-memory-dump-allocator-details-pane'); + if (pid === undefined) { + assert.isUndefined(allocatorDetailsPaneEl); + assert.isUndefined(allocatorName); // Test sanity check. + assert.isUndefined(withHeapDetailsPane); // Test sanity check. + return; + } + + assert.deepEqual( + Array.from(allocatorDetailsPaneEl.memoryAllocatorDumps), + extractMemoryAllocatorDumps(processMemoryDumps, pid, allocatorName)); + assert.strictEqual( + allocatorDetailsPaneEl.aggregationMode, headerPaneEl.aggregationMode); + + const heapDetailsPaneEl = tr.ui.b.findDeepElementMatching( + stackedPaneViewEl, 'tr-ui-a-memory-dump-heap-details-pane'); + if (!withHeapDetailsPane) { + assert.isUndefined(heapDetailsPaneEl); + return; + } + + assert.deepEqual(Array.from(heapDetailsPaneEl.heapDumps), + extractHeapDumps(processMemoryDumps, pid, allocatorName)); + assert.strictEqual( + heapDetailsPaneEl.aggregationMode, headerPaneEl.aggregationMode); + } + + detailsCheckCallback( + overviewTableEl, checkVmRegionsPane, checkAllocatorPane); + } + + test('instantiate_empty', function() { + // All these views should be completely empty. + const unsetViewEl = document.createElement( + 'tr-ui-a-container-memory-dump-sub-view'); + this.addHTMLOutput(unsetViewEl); + assert.strictEqual(unsetViewEl.getBoundingClientRect().width, 0); + assert.strictEqual(unsetViewEl.getBoundingClientRect().height, 0); + + const undefinedViewEl = createViewWithSelection(undefined); + this.addHTMLOutput(undefinedViewEl); + assert.strictEqual(undefinedViewEl.getBoundingClientRect().width, 0); + assert.strictEqual(undefinedViewEl.getBoundingClientRect().height, 0); + + const emptyViewEl = createViewWithSelection([]); + this.addHTMLOutput(emptyViewEl); + assert.strictEqual(emptyViewEl.getBoundingClientRect().width, 0); + assert.strictEqual(emptyViewEl.getBoundingClientRect().height, 0); + }); + + test('instantiate_singleGlobalMemoryDump', function() { + createAndCheckContainerMemoryDumpView(this, + [tr.ui.analysis.createSingleTestGlobalMemoryDump()], + function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) { + // Nothing should be selected initially. + assert.isUndefined(overviewTableEl.selectedTableRow); + assert.isUndefined(overviewTableEl.selectedColumnIndex); + checkVmRegionsPane(undefined); + checkAllocatorPane(undefined); + + // Total resident of Process 1. + overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0]; + overviewTableEl.selectedColumnIndex = 1; + checkVmRegionsPane(1 /* PID */); + checkAllocatorPane(undefined); + + // PSS of process 4. + overviewTableEl.selectedColumnIndex = 3; + overviewTableEl.selectedTableRow = overviewTableEl.tableRows[2]; + checkVmRegionsPane(undefined); + checkAllocatorPane(undefined); + + // Malloc of process 2. + overviewTableEl.selectedTableRow = overviewTableEl.tableRows[1]; + overviewTableEl.selectedColumnIndex = 10; + checkVmRegionsPane(undefined); + checkAllocatorPane(2 /* PID */, 'malloc', + false /* no heap details pane */); + }); + }); + + test('instantiate_multipleGlobalMemoryDumps', function() { + createAndCheckContainerMemoryDumpView(this, + tr.ui.analysis.createMultipleTestGlobalMemoryDumps(), + function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) { + // Nothing should be selected initially. + assert.isUndefined(overviewTableEl.selectedTableRow); + assert.isUndefined(overviewTableEl.selectedColumnIndex); + checkVmRegionsPane(undefined); + checkAllocatorPane(undefined); + + // Blink of Process 1. + overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0]; + overviewTableEl.selectedColumnIndex = 8; + checkVmRegionsPane(undefined); + checkAllocatorPane(undefined); + + // Peak total resident of Process 4. + overviewTableEl.selectedTableRow = overviewTableEl.tableRows[3]; + overviewTableEl.selectedColumnIndex = 2; + checkVmRegionsPane(undefined); + checkAllocatorPane(undefined); + + // V8 of Process 3. + overviewTableEl.selectedTableRow = overviewTableEl.tableRows[2]; + overviewTableEl.selectedColumnIndex = 12; + checkVmRegionsPane(undefined); + checkAllocatorPane(3 /* PID */, 'v8', true /* heap details pane */); + }); + }); + + test('instantiate_singleProcessMemoryDump', function() { + createAndCheckContainerMemoryDumpView(this, + [tr.ui.analysis.createSingleTestProcessMemoryDump()], + function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) { + // Nothing should be selected initially. + assert.isUndefined(overviewTableEl.selectedTableRow); + assert.isUndefined(overviewTableEl.selectedColumnIndex); + checkVmRegionsPane(undefined); + checkAllocatorPane(undefined); + + // Tracing of Process 2. + overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0]; + overviewTableEl.selectedColumnIndex = 13; + checkVmRegionsPane(undefined); + checkAllocatorPane(2 /* PID */, 'tracing', + false /* no heap details pane */); + + // Blink of Process 2. + overviewTableEl.selectedColumnIndex = 8; + checkVmRegionsPane(undefined); + checkAllocatorPane(2 /* PID */, 'blink', + false /* no heap details pane */); + + // Total resident of Process 2. + overviewTableEl.selectedColumnIndex = 1; + checkVmRegionsPane(2 /* PID */); + checkAllocatorPane(undefined); + }); + }); + + test('instantiate_multipleProcessMemoryDumps', function() { + createAndCheckContainerMemoryDumpView(this, + tr.ui.analysis.createMultipleTestProcessMemoryDumps(), + function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) { + // Nothing should be selected initially. + assert.isUndefined(overviewTableEl.selectedTableRow); + assert.isUndefined(overviewTableEl.selectedColumnIndex); + checkVmRegionsPane(undefined); + checkAllocatorPane(undefined); + + // Tracing of Process 2. + overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0]; + overviewTableEl.selectedColumnIndex = 13; + checkVmRegionsPane(undefined); + checkAllocatorPane(2 /* PID */, 'tracing', + false /* no heap details pane */); + + // V8 of Process 2. + overviewTableEl.selectedColumnIndex = 12; + checkVmRegionsPane(undefined); + checkAllocatorPane(2 /* PID */, 'v8', + false /* no heap details pane */); + + // PSS of Process 2. + overviewTableEl.selectedColumnIndex = 3; + checkVmRegionsPane(2 /* PID */); + checkAllocatorPane(undefined); + }); + }); + + test('memory', function() { + const containerEl = document.createElement('div'); + containerEl.brushingStateController = + new tr.c.BrushingStateController(undefined); + + // Create the first container memory view. + createAndCheckContainerMemoryDumpView(this, + [tr.ui.analysis.createSingleTestProcessMemoryDump()], + function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) { + // Nothing should be selected initially. + assert.isUndefined(overviewTableEl.selectedTableRow); + assert.isUndefined(overviewTableEl.selectedColumnIndex); + checkVmRegionsPane(undefined); + checkAllocatorPane(undefined); + + // Select V8 of Process 2. + overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0]; + overviewTableEl.selectedColumnIndex = 12; + checkVmRegionsPane(undefined); + checkAllocatorPane(2 /* PID */, 'v8', + false /* no heap details pane */); + }, containerEl); + + // Destroy the first container memory view. + Polymer.dom(containerEl).textContent = ''; + + // Create the second container memory view. + createAndCheckContainerMemoryDumpView(this, + tr.ui.analysis.createMultipleTestGlobalMemoryDumps(), + function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) { + // V8 of Process 2 should still be selected (even though the selection + // changed). + assert.strictEqual( + overviewTableEl.selectedTableRow, overviewTableEl.tableRows[1]); + assert.strictEqual(overviewTableEl.selectedColumnIndex, 12); + checkVmRegionsPane(undefined); + checkAllocatorPane(2 /* PID */, 'v8', + false /* no heap details pane */); + }, containerEl); + }); + + test('instantiate_differentProcessMemoryDumps', function() { + const globalMemoryDumps = + tr.ui.analysis.createMultipleTestGlobalMemoryDumps(); + // 2 dumps in Process 1, 3 dumps in Process 2, and 1 dump in Process 4 + // (intentionally shuffled to check sorting). + const differentProcessDumps = [ + globalMemoryDumps[1].processMemoryDumps[2], + globalMemoryDumps[0].processMemoryDumps[1], + globalMemoryDumps[0].processMemoryDumps[2], + globalMemoryDumps[1].processMemoryDumps[4], + globalMemoryDumps[1].processMemoryDumps[1], + globalMemoryDumps[2].processMemoryDumps[2] + ]; + + const viewEl = createViewWithSelection(differentProcessDumps); + this.addHTMLOutput(viewEl); + + const tableEl = tr.ui.b.findDeepElementMatching(viewEl, 'tr-ui-b-table'); + assert.lengthOf(tableEl.tableRows, 3); + assert.lengthOf(tableEl.tableColumns, 1); + const rows = tableEl.tableRows; + const col = tableEl.tableColumns[0]; + + assert.strictEqual(Polymer.dom(col.value(rows[0])).textContent, + '2 memory dumps in Process 1'); + assert.strictEqual(Polymer.dom(col.value(rows[1])).textContent, + '3 memory dumps in Process 2'); + assert.strictEqual(Polymer.dom(col.value(rows[2])).textContent, + '1 memory dump in Process 4'); + + // Check that the analysis link is associated with the right dumps. + assert.isTrue(col.value(rows[1]).selection.equals(new tr.model.EventSet([ + globalMemoryDumps[0].processMemoryDumps[2], + globalMemoryDumps[1].processMemoryDumps[2], + globalMemoryDumps[2].processMemoryDumps[2] + ]))); + + assert.lengthOf(rows[1].subRows, 3); + const subRow = rows[1].subRows[0]; + + // Check the timestamp. + assert.strictEqual(col.value(subRow).children[0].value, 42); + + // Check that the analysis link is associated with the right dump. + assert.isTrue(col.value(subRow).selection.equals( + new tr.model.EventSet(globalMemoryDumps[0].processMemoryDumps[2]))); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view.html new file mode 100644 index 00000000000..a9275b19d0c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view.html @@ -0,0 +1,139 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-a-counter-sample-sub-view'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + tr-ui-b-table { + font-size: 12px; + } + </style> + <tr-ui-b-table id='table'></tr-ui-b-table> + </template> +</dom-module> + +<script> +'use strict'; +(function() { + const COUNTER_SAMPLE_TABLE_COLUMNS = [ + { + title: 'Counter', + width: '150px', + value(row) { return row.counter; } + }, + { + title: 'Series', + width: '150px', + value(row) { return row.series; } + }, + { + title: 'Time', + width: '150px', + value(row) { return row.start; } + }, + { + title: 'Value', + width: '100%', + value(row) { return row.value; } + } + ]; + + Polymer({ + is: 'tr-ui-a-counter-sample-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + ready() { + this.currentSelection_ = undefined; + this.$.table.tableColumns = COUNTER_SAMPLE_TABLE_COLUMNS; + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + this.currentSelection_ = selection; + this.updateContents_(); + }, + + updateContents_() { + this.$.table.tableRows = + this.selection ? this.getRows_(this.selection.toArray()) : []; + this.$.table.rebuild(); + }, + + /** + * Returns the table rows for the specified samples. + * + * We print each counter/series combination the first time that it + * appears. For subsequent samples in each series, we omit the counter + * and series name. This makes it easy to scan to find the next series. + * + * Each series can be collapsed. In the expanded state, all samples + * are shown. In the collapsed state, only the first sample is displayed. + */ + getRows_(samples) { + const samplesByCounter = tr.b.groupIntoMap( + samples, sample => sample.series.counter.guid); + + const rows = []; + for (const counterSamples of samplesByCounter.values()) { + const samplesBySeries = tr.b.groupIntoMap( + counterSamples, sample => sample.series.guid); + + for (const seriesSamples of samplesBySeries.values()) { + const seriesRows = this.getRowsForSamples_(seriesSamples); + seriesRows[0].counter = seriesSamples[0].series.counter.name; + seriesRows[0].series = seriesSamples[0].series.name; + + if (seriesRows.length > 1) { + seriesRows[0].subRows = seriesRows.slice(1); + seriesRows[0].isExpanded = true; + } + + rows.push(seriesRows[0]); + } + } + + return rows; + }, + + getRowsForSamples_(samples) { + return samples.map(function(sample) { + return { + start: sample.timestamp, + value: sample.value + }; + }); + } + }); + + tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-counter-sample-sub-view', + tr.model.CounterSample, + { + multi: false, + title: 'Counter Sample', + }); + + tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-counter-sample-sub-view', + tr.model.CounterSample, + { + multi: true, + title: 'Counter Samples', + }); +})(); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view_test.html new file mode 100644 index 00000000000..9d7fa370313 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view_test.html @@ -0,0 +1,177 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/counter.html"> +<link rel="import" href="/tracing/model/counter_series.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/counter_sample_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Counter = tr.model.Counter; + const CounterSeries = tr.model.CounterSeries; + const EventSet = tr.model.EventSet; + + test('instantiate_undefinedSelection', function() { + const analysisEl = document.createElement( + 'tr-ui-a-counter-sample-sub-view'); + analysisEl.selection = new EventSet(undefined); + + assert.lengthOf(analysisEl.$.table.tableRows, 0); + }); + + test('instantiate_oneCounterOneSeries', function() { + const series = new CounterSeries('series1', 0); + series.addCounterSample(0, 0); + series.addCounterSample(1, 10); + + const counter = new Counter(null, 0, 'cat', 'ctr1'); + counter.addSeries(series); + + const analysisEl = document.createElement( + 'tr-ui-a-counter-sample-sub-view'); + analysisEl.selection = new EventSet(series.samples); + this.addHTMLOutput(analysisEl); + + // The first sample should be listed as a collapsible header row for the + // series. + const rows = analysisEl.$.table.tableRows; + assert.lengthOf(rows, 1); + assert.isTrue(rows[0].isExpanded); + assert.strictEqual(rows[0].counter, 'ctr1'); + assert.strictEqual(rows[0].series, 'series1'); + assert.strictEqual(rows[0].start, 0); + assert.strictEqual(rows[0].value, 0); + + // The second sample should be listed as a subrow of the first. + const subRows = rows[0].subRows; + assert.lengthOf(subRows, 1); + assert.isUndefined(subRows[0].counter); + assert.isUndefined(subRows[0].series); + assert.strictEqual(subRows[0].start, 1); + assert.strictEqual(subRows[0].value, 10); + }); + + test('instantiate_singleSampleDoesntHaveSubrows', function() { + const series = new CounterSeries('series1', 0); + series.addCounterSample(0, 0); + + const counter = new Counter(null, 0, 'cat', 'ctr1'); + counter.addSeries(series); + + const analysisEl = document.createElement( + 'tr-ui-a-counter-sample-sub-view'); + analysisEl.selection = new EventSet(series.samples); + this.addHTMLOutput(analysisEl); + + // The first sample should be listed as a collapsible header row for the + // series. + const rows = analysisEl.$.table.tableRows; + assert.lengthOf(rows, 1); + assert.strictEqual(rows[0].counter, 'ctr1'); + assert.strictEqual(rows[0].series, 'series1'); + assert.strictEqual(rows[0].start, 0); + assert.strictEqual(rows[0].value, 0); + assert.isUndefined(rows[0].subRows); + }); + + test('instantiate_oneCounterTwoSeries', function() { + const series1 = new CounterSeries('series1', 0); + series1.addCounterSample(1, 10); + series1.addCounterSample(2, 20); + + const series2 = new CounterSeries('series2', 0); + series2.addCounterSample(3, 30); + + const counter = new Counter(null, 0, 'cat', 'ctr1'); + counter.addSeries(series1); + counter.addSeries(series2); + + const analysisEl = document.createElement( + 'tr-ui-a-counter-sample-sub-view'); + analysisEl.selection = + new EventSet(series1.samples.concat(series2.samples)); + this.addHTMLOutput(analysisEl); + + // The first samples should be listed as collapsible header rows for the + // series. + const rows = analysisEl.$.table.tableRows; + assert.lengthOf(rows, 2); + assert.strictEqual(rows[0].counter, 'ctr1'); + assert.strictEqual(rows[0].series, 'series1'); + assert.strictEqual(rows[0].start, 1); + assert.strictEqual(rows[0].value, 10); + + assert.strictEqual(rows[1].counter, 'ctr1'); + assert.strictEqual(rows[1].series, 'series2'); + assert.strictEqual(rows[1].start, 3); + assert.strictEqual(rows[1].value, 30); + + // The subsequent samples should be listed as subrows of the first. + const subRows1 = rows[0].subRows; + assert.lengthOf(subRows1, 1); + assert.isUndefined(subRows1[0].counter); + assert.isUndefined(subRows1[0].series); + assert.strictEqual(subRows1[0].start, 2); + assert.strictEqual(subRows1[0].value, 20); + + assert.isUndefined(rows[1].subRows); + }); + + test('instantiate_twoCountersTwoSeries', function() { + const series1 = new CounterSeries('series1', 0); + series1.addCounterSample(1, 10); + + const series2 = new CounterSeries('series2', 0); + series2.addCounterSample(2, 20); + + const counter1 = new Counter(null, 0, 'cat', 'ctr1'); + const counter2 = new Counter(null, 0, 'cat', 'ctr2'); + counter1.addSeries(series1); + counter2.addSeries(series2); + + const analysisEl = document.createElement( + 'tr-ui-a-counter-sample-sub-view'); + analysisEl.selection = + new EventSet(series1.samples.concat(series2.samples)); + this.addHTMLOutput(analysisEl); + + // Each sample should be a header row with no subrows. + const rows = analysisEl.$.table.tableRows; + assert.lengthOf(rows, 2); + assert.strictEqual(rows[0].counter, 'ctr1'); + assert.strictEqual(rows[0].series, 'series1'); + assert.strictEqual(rows[0].start, 1); + assert.strictEqual(rows[0].value, 10); + assert.isUndefined(rows[0].subRows); + + assert.strictEqual(rows[1].counter, 'ctr2'); + assert.strictEqual(rows[1].series, 'series2'); + assert.strictEqual(rows[1].start, 2); + assert.strictEqual(rows[1].value, 20); + assert.isUndefined(rows[1].subRows); + }); + + test('instantiate_contentsClearedEachSelection', function() { + const series = new CounterSeries('series1', 0); + series.addCounterSample(0, 0); + + const counter = new Counter(null, 0, 'cat', 'ctr1'); + counter.addSeries(series); + + const analysisEl = document.createElement( + 'tr-ui-a-counter-sample-sub-view'); + analysisEl.selection = new EventSet(series.samples); + analysisEl.selection = new EventSet(series.samples); + this.addHTMLOutput(analysisEl); + + assert.lengthOf(analysisEl.$.table.tableRows, 1); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier.html new file mode 100644 index 00000000000..1773a09f32f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const FLOW_IN = 0x1; + const FLOW_OUT = 0x2; + const FLOW_IN_OUT = FLOW_IN | FLOW_OUT; + + function FlowClassifier() { + this.numEvents_ = 0; + this.eventsByGUID_ = {}; + } + + FlowClassifier.prototype = { + getFS_(event) { + let fs = this.eventsByGUID_[event.guid]; + if (fs === undefined) { + this.numEvents_++; + fs = { + state: 0, + event + }; + this.eventsByGUID_[event.guid] = fs; + } + return fs; + }, + + addInFlow(event) { + const fs = this.getFS_(event); + fs.state |= FLOW_IN; + return event; + }, + + addOutFlow(event) { + const fs = this.getFS_(event); + fs.state |= FLOW_OUT; + return event; + }, + + hasEvents() { + return this.numEvents_ > 0; + }, + + get inFlowEvents() { + const selection = new tr.model.EventSet(); + for (const guid in this.eventsByGUID_) { + const fs = this.eventsByGUID_[guid]; + if (fs.state === FLOW_IN) { + selection.push(fs.event); + } + } + return selection; + }, + + get outFlowEvents() { + const selection = new tr.model.EventSet(); + for (const guid in this.eventsByGUID_) { + const fs = this.eventsByGUID_[guid]; + if (fs.state === FLOW_OUT) { + selection.push(fs.event); + } + } + return selection; + }, + + get internalFlowEvents() { + const selection = new tr.model.EventSet(); + for (const guid in this.eventsByGUID_) { + const fs = this.eventsByGUID_[guid]; + if (fs.state === FLOW_IN_OUT) { + selection.push(fs.event); + } + } + return selection; + } + }; + + return { + FlowClassifier, + }; +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier_test.html new file mode 100644 index 00000000000..ba68f671b57 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier_test.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/analysis/flow_classifier.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newFlowEventEx = tr.c.TestUtils.newFlowEventEx; + + test('basic', function() { + const a = newFlowEventEx({ + title: 'a', start: 0, end: 10 }); + const b = newFlowEventEx({ + title: 'b', start: 10, end: 20 }); + const c = newFlowEventEx({ + title: 'c', start: 20, end: 25 }); + const d = newFlowEventEx({ + title: 'd', start: 30, end: 35 }); + + const fc = new tr.ui.analysis.FlowClassifier(); + fc.addInFlow(a); + + fc.addInFlow(b); + fc.addOutFlow(b); + + fc.addInFlow(c); + fc.addOutFlow(c); + + fc.addOutFlow(d); + + function asSortedArray(selection) { + const events = Array.from(selection); + events.sort(function(a, b) { + return a.guid - b.guid; + }); + return events; + } + + assert.deepEqual(Array.from(fc.inFlowEvents), [a]); + assert.deepEqual(Array.from(fc.outFlowEvents), [d]); + assert.deepEqual(Array.from(fc.internalFlowEvents), [b, c]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart.html new file mode 100644 index 00000000000..bc3f4fcead8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart.html @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/base/line_chart.html"> + +<!-- +@fileoverview A line chart showing milliseconds since the start of the frame on +the x-axis and power consumption on the y-axis. Each frame is shown as a +separate line in the chart. Vertical sync events are used as the start of each +frame. + +This chart aims to help users understand the shape of the power consumption +curve over the course of a frame or set of frames. +--> +<dom-module id='tr-ui-a-frame-power-usage-chart'> + <template> + <div id="content"></div> + </template> +</dom-module> + +<script> +'use strict'; + +const EventSet = tr.model.EventSet; + +const CHART_TITLE = 'Power (W) by ms since vertical sync'; + +Polymer({ + is: 'tr-ui-a-frame-power-usage-chart', + + ready() { + this.chart_ = undefined; + this.samples_ = new EventSet(); + this.vSyncTimestamps_ = []; + }, + + attached() { + if (this.samples_) this.updateContents_(); + }, + + get chart() { + return this.chart_; + }, + + get samples() { + return this.samples_; + }, + + get vSyncTimestamps() { + return this.vSyncTimestamps_; + }, + + /** + * Sets the data that powers the chart. Vsync timestamps must be in + * chronological order. + */ + setData(samples, vSyncTimestamps) { + this.samples_ = (samples === undefined) ? new EventSet() : samples; + this.vSyncTimestamps_ = + (vSyncTimestamps === undefined) ? [] : vSyncTimestamps; + if (this.isAttached) this.updateContents_(); + }, + + updateContents_() { + this.clearChart_(); + + const data = this.getDataForLineChart_(); + + if (data.length === 0) return; + + this.chart_ = new tr.ui.b.LineChart(); + Polymer.dom(this.$.content).appendChild(this.chart_); + this.chart_.chartTitle = CHART_TITLE; + this.chart_.data = data; + }, + + clearChart_() { + const content = this.$.content; + while (Polymer.dom(content).firstChild) { + Polymer.dom(content).removeChild(Polymer.dom(content).firstChild); + } + + this.chart_ = undefined; + }, + + // TODO(charliea): Limit the ms since vsync to the median frame length. The + // vertical syncs are not 100% regular and highlighting any sample that's + // in one of these 'vertical sync lulls' makes the x-axis have a much larger + // scale than it should, effectively squishing the other samples into the + // left side of the chart. + /** + * Returns an array of data points for the chart. Each element in the array + * is of the form { x: <ms since vsync>, f<frame#>: <power in mW> }. + */ + getDataForLineChart_() { + const sortedSamples = this.sortSamplesByTimestampAscending_(this.samples); + const vSyncTimestamps = this.vSyncTimestamps.slice(); + + let lastVSyncTimestamp = undefined; + const points = []; + + // For each power sample, find and record the frame number that it belongs + // to as well as the amount of time elapsed since that frame began. + let frameNumber = 0; + sortedSamples.forEach(function(sample) { + while (vSyncTimestamps.length > 0 && vSyncTimestamps[0] <= sample.start) { + lastVSyncTimestamp = vSyncTimestamps.shift(); + frameNumber++; + } + + // If no vertical sync occurred before the power sample, don't use the + // power sample. + if (lastVSyncTimestamp === undefined) return; + + const point = { x: sample.start - lastVSyncTimestamp }; + point['f' + frameNumber] = sample.powerInW; + points.push(point); + }); + + return points; + }, + + sortSamplesByTimestampAscending_(samples) { + return samples.toArray().sort(function(smpl1, smpl2) { + return smpl1.start - smpl2.start; + }); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_perf_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_perf_test.html new file mode 100644 index 00000000000..caf4601f33c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_perf_test.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/power_sample.html"> +<link rel="import" href="/tracing/model/power_series.html"> +<link rel="import" href="/tracing/ui/analysis/frame_power_usage_chart.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function instantiateManyFrames() { + const model = new tr.Model(); + const numFrames = 200; + const samplesPerFrame = 200; + + // Set up the test data. + const series = new tr.model.PowerSeries(model.device); + const vsyncTimestamps = []; + for (let i = 0; i < numFrames; i++) { + vsyncTimestamps.push(i * samplesPerFrame); + for (let j = 0; j < samplesPerFrame; j++) { + series.addPowerSample(vsyncTimestamps[i] + j, j); + } + } + const samples = series.samples; + + // Display the chart. + const chart = document.createElement('tr-ui-a-frame-power-usage-chart'); + chart.setData(new tr.model.EventSet(samples), vsyncTimestamps); + this.addHTMLOutput(chart); + } + + timedPerfTest('frame_power_usage_chart', instantiateManyFrames, { + iterations: 1 + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_test.html new file mode 100644 index 00000000000..04ba9388852 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_test.html @@ -0,0 +1,267 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/power_sample.html"> +<link rel="import" href="/tracing/model/power_series.html"> +<link rel="import" href="/tracing/ui/analysis/frame_power_usage_chart.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate_noSamples', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + const chart = document.createElement('tr-ui-a-frame-power-usage-chart'); + chart.setData(undefined, [0]); + + assert.isUndefined(chart.chart); + }); + + test('instantiate_noVSyncs', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + series.addPowerSample(0, 1); + series.addPowerSample(1, 2); + series.addPowerSample(2, 3); + series.addPowerSample(3, 2); + + const chart = document.createElement('tr-ui-a-frame-power-usage-chart'); + chart.setData(new tr.model.EventSet(series.samples), []); + + assert.isUndefined(chart.chart); + }); + + test('instantiate_noSamplesOrVSyncs', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + const chart = document.createElement('tr-ui-a-frame-power-usage-chart'); + chart.setData(undefined, []); + + assert.isUndefined(chart.chart); + }); + + test('instantiate_oneFrame', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + const vSyncTimestamps = [0]; + series.addPowerSample(0, 1); + series.addPowerSample(1, 2); + series.addPowerSample(2, 3); + series.addPowerSample(3, 2); + + const chart = document.createElement('tr-ui-a-frame-power-usage-chart'); + chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps); + + this.addHTMLOutput(chart); + + const expectedChartData = [ + { x: 0, f1: 1 }, + { x: 1, f1: 2 }, + { x: 2, f1: 3 }, + { x: 3, f1: 2 } + ]; + assert.sameDeepMembers(chart.chart.data, expectedChartData); + }); + + test('instantiate_twoFrames', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + const vSyncTimestamps = [0, 4]; + series.addPowerSample(0, 1); + series.addPowerSample(1, 2); + series.addPowerSample(2, 3); + series.addPowerSample(3, 2); + series.addPowerSample(4, 2); + series.addPowerSample(5, 3); + series.addPowerSample(6, 4); + series.addPowerSample(7, 3); + + const chart = document.createElement('tr-ui-a-frame-power-usage-chart'); + chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps); + + this.addHTMLOutput(chart); + + const expectedChartData = [ + { x: 0, f1: 1 }, + { x: 1, f1: 2 }, + { x: 2, f1: 3 }, + { x: 3, f1: 2 }, + { x: 0, f2: 2 }, + { x: 1, f2: 3 }, + { x: 2, f2: 4 }, + { x: 3, f2: 3 } + ]; + assert.sameDeepMembers(chart.chart.data, expectedChartData); + }); + + test('instantiate_twoFramesDifferentXValues', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + // Power samples taken at 0, 1, 2, and 3s after frame start. + const vSyncTimestamps = [0, 4]; + series.addPowerSample(0, 1); + series.addPowerSample(1, 2); + series.addPowerSample(2, 3); + series.addPowerSample(3, 2); + // Power samples taken at 0.5, 1.5, 2.5, and 3.5s after frame start. + series.addPowerSample(4.5, 2); + series.addPowerSample(5.5, 3); + series.addPowerSample(6.5, 4); + series.addPowerSample(7.5, 3); + + const chart = document.createElement('tr-ui-a-frame-power-usage-chart'); + chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps); + + this.addHTMLOutput(chart); + + const expectedChartData = [ + { x: 0, f1: 1 }, + { x: 1, f1: 2 }, + { x: 2, f1: 3 }, + { x: 3, f1: 2 }, + { x: 0.5, f2: 2 }, + { x: 1.5, f2: 3 }, + { x: 2.5, f2: 4 }, + { x: 3.5, f2: 3 } + ]; + assert.sameDeepMembers(chart.chart.data, expectedChartData); + }); + + test('instantiate_samplesBeforeFirstVSync', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + const vSyncTimestamps = [4]; + series.addPowerSample(0, 1); + series.addPowerSample(1, 2); + series.addPowerSample(2, 3); + series.addPowerSample(3, 2); + series.addPowerSample(4, 2); + series.addPowerSample(5, 3); + series.addPowerSample(6, 4); + series.addPowerSample(7, 3); + + const chart = document.createElement('tr-ui-a-frame-power-usage-chart'); + chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps); + + this.addHTMLOutput(chart); + + const expectedChartData = [ + { x: 0, f1: 2 }, + { x: 1, f1: 3 }, + { x: 2, f1: 4 }, + { x: 3, f1: 3 } + ]; + assert.sameDeepMembers(chart.chart.data, expectedChartData); + }); + + test('instantiate_allSamplesBeforeFirstVSync', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + const vSyncTimestamps = [4]; + series.addPowerSample(0, 1); + series.addPowerSample(1, 2); + series.addPowerSample(2, 3); + series.addPowerSample(3, 2); + + const chart = document.createElement('tr-ui-a-frame-power-usage-chart'); + chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps); + + const expectedChartData = [ + { x: 0, f1: 2 }, + { x: 1, f1: 3 }, + { x: 2, f1: 4 }, + { x: 3, f1: 3 } + ]; + assert.isUndefined(chart.chart); + }); + + test('instantiate_vSyncsAfterLastPowerSample', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + const vSyncTimestamps = [0, 4, 8, 12]; + series.addPowerSample(0, 1); + series.addPowerSample(1, 2); + series.addPowerSample(2, 3); + series.addPowerSample(3, 2); + series.addPowerSample(4, 2); + series.addPowerSample(5, 3); + series.addPowerSample(6, 4); + series.addPowerSample(7, 3); + + const chart = document.createElement('tr-ui-a-frame-power-usage-chart'); + chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps); + + this.addHTMLOutput(chart); + + const expectedChartData = [ + { x: 0, f1: 1 }, + { x: 1, f1: 2 }, + { x: 2, f1: 3 }, + { x: 3, f1: 2 }, + { x: 0, f2: 2 }, + { x: 1, f2: 3 }, + { x: 2, f2: 4 }, + { x: 3, f2: 3 } + ]; + assert.sameDeepMembers(chart.chart.data, expectedChartData); + }); + + test('instantiate_onlyVSyncAfterLastPowerSample', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + const vSyncTimestamps = [8]; + series.addPowerSample(0, 1); + series.addPowerSample(1, 2); + series.addPowerSample(2, 3); + series.addPowerSample(3, 2); + series.addPowerSample(4, 2); + series.addPowerSample(5, 3); + series.addPowerSample(6, 4); + series.addPowerSample(7, 3); + + const chart = document.createElement('tr-ui-a-frame-power-usage-chart'); + chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps); + + assert.isUndefined(chart.chart); + }); + + + test('instantiate_samplesNotInChronologicalOrder', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + const vSyncTimestamps = [0, 4]; + series.addPowerSample(4, 2); + series.addPowerSample(5, 3); + series.addPowerSample(6, 4); + series.addPowerSample(7, 3); + series.addPowerSample(0, 1); + series.addPowerSample(1, 2); + series.addPowerSample(2, 3); + series.addPowerSample(3, 2); + + const chart = document.createElement('tr-ui-a-frame-power-usage-chart'); + chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps); + + this.addHTMLOutput(chart); + + const expectedChartData = [ + { x: 0, f1: 1 }, + { x: 1, f1: 2 }, + { x: 2, f1: 3 }, + { x: 3, f1: 2 }, + { x: 0, f2: 2 }, + { x: 1, f2: 3 }, + { x: 2, f2: 4 }, + { x: 3, f2: 3 } + ]; + assert.sameDeepMembers(chart.chart.data, expectedChartData); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view.html new file mode 100644 index 00000000000..e0c33df1231 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view.html @@ -0,0 +1,347 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/rect.html"> +<link rel="import" href="/tracing/base/scalar.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/object_instance.html"> +<link rel="import" href="/tracing/model/object_snapshot.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-generic-object-view'> + <template> + <style> + :host { + display: block; + font-family: monospace; + } + </style> + <div id="content"> + </div> + </template> +</dom-module> +<script> +'use strict'; + +function isTable(object) { + if (!(object instanceof Array) || + (object.length < 2)) return false; + for (const colName in object[0]) { + if (typeof colName !== 'string') return false; + } + for (let i = 0; i < object.length; ++i) { + if (!(object[i] instanceof Object)) return false; + for (const colName in object[i]) { + if (i && (object[0][colName] === undefined)) return false; + const cellType = typeof object[i][colName]; + if (cellType !== 'string' && cellType !== 'number') return false; + } + if (i) { + for (const colName in object[0]) { + if (object[i][colName] === undefined) return false; + } + } + } + return true; +} + +Polymer({ + is: 'tr-ui-a-generic-object-view', + + ready() { + this.object_ = undefined; + }, + + get object() { + return this.object_; + }, + + set object(object) { + this.object_ = object; + this.updateContents_(); + }, + + updateContents_() { + Polymer.dom(this.$.content).textContent = ''; + this.appendElementsForType_('', this.object_, 0, 0, 5, ''); + }, + + appendElementsForType_( + label, object, indent, depth, maxDepth, suffix) { + if (depth > maxDepth) { + this.appendSimpleText_( + label, indent, '<recursion limit reached>', suffix); + return; + } + + if (object === undefined) { + this.appendSimpleText_(label, indent, 'undefined', suffix); + return; + } + + if (object === null) { + this.appendSimpleText_(label, indent, 'null', suffix); + return; + } + + if (!(object instanceof Object)) { + const type = typeof object; + if (type !== 'string') { + return this.appendSimpleText_(label, indent, object, suffix); + } + let objectReplaced = false; + if ((object[0] === '{' && object[object.length - 1] === '}') || + (object[0] === '[' && object[object.length - 1] === ']')) { + try { + object = JSON.parse(object); + objectReplaced = true; + } catch (e) { + } + } + if (!objectReplaced) { + if (object.includes('\n')) { + const lines = object.split('\n'); + lines.forEach(function(line, i) { + let text; + let ioff; + let ll; + let ss; + if (i === 0) { + text = '"' + line; + ioff = 0; + ll = label; + ss = ''; + } else if (i < lines.length - 1) { + text = line; + ioff = 1; + ll = ''; + ss = ''; + } else { + text = line + '"'; + ioff = 1; + ll = ''; + ss = suffix; + } + + const el = this.appendSimpleText_( + ll, indent + ioff * label.length + ioff, text, ss); + el.style.whiteSpace = 'pre'; + return el; + }, this); + return; + } + if (tr.b.isUrl(object)) { + const link = document.createElement('a'); + link.href = object; + link.textContent = object; + this.appendElementWithLabel_(label, indent, link, suffix); + return; + } + this.appendSimpleText_( + label, indent, '"' + object + '"', suffix); + return; + } + } + + if (object instanceof tr.model.ObjectSnapshot) { + const link = document.createElement('tr-ui-a-analysis-link'); + link.selection = new tr.model.EventSet(object); + this.appendElementWithLabel_(label, indent, link, suffix); + return; + } + + if (object instanceof tr.model.ObjectInstance) { + const link = document.createElement('tr-ui-a-analysis-link'); + link.selection = new tr.model.EventSet(object); + this.appendElementWithLabel_(label, indent, link, suffix); + return; + } + + if (object instanceof tr.b.math.Rect) { + this.appendSimpleText_(label, indent, object.toString(), suffix); + return; + } + + if (object instanceof tr.b.Scalar) { + const el = this.ownerDocument.createElement('tr-v-ui-scalar-span'); + el.value = object; + el.inline = true; + this.appendElementWithLabel_(label, indent, el, suffix); + return; + } + + if (object instanceof Array) { + this.appendElementsForArray_( + label, object, indent, depth, maxDepth, suffix); + return; + } + + this.appendElementsForObject_( + label, object, indent, depth, maxDepth, suffix); + }, + + appendElementsForArray_( + label, object, indent, depth, maxDepth, suffix) { + if (object.length === 0) { + this.appendSimpleText_(label, indent, '[]', suffix); + return; + } + + if (isTable(object)) { + const table = document.createElement('tr-ui-b-table'); + const columns = []; + for (const colName of Object.keys(object[0])) { + let allStrings = true; + let allNumbers = true; + for (let i = 0; i < object.length; ++i) { + if (typeof(object[i][colName]) !== 'string') { + allStrings = false; + } + + if (typeof(object[i][colName]) !== 'number') { + allNumbers = false; + } + + if (!allStrings && !allNumbers) break; + } + + const column = {title: colName}; + column.value = function(row) { + return row[colName]; + }; + + if (allStrings) { + column.cmp = function(x, y) { + return x[colName].localeCompare(y[colName]); + }; + } else if (allNumbers) { + column.cmp = function(x, y) { + return x[colName] - y[colName]; + }; + } + columns.push(column); + } + table.tableColumns = columns; + table.tableRows = object; + this.appendElementWithLabel_(label, indent, table, suffix); + table.rebuild(); + return; + } + + this.appendElementsForType_( + label + '[', + object[0], + indent, depth + 1, maxDepth, + object.length > 1 ? ',' : ']' + suffix); + for (let i = 1; i < object.length; i++) { + this.appendElementsForType_( + '', + object[i], + indent + label.length + 1, depth + 1, maxDepth, + i < object.length - 1 ? ',' : ']' + suffix); + } + return; + }, + + appendElementsForObject_( + label, object, indent, depth, maxDepth, suffix) { + const keys = Object.keys(object); + if (keys.length === 0) { + this.appendSimpleText_(label, indent, '{}', suffix); + return; + } + + this.appendElementsForType_( + label + '{' + keys[0] + ': ', + object[keys[0]], + indent, depth, maxDepth, + keys.length > 1 ? ',' : '}' + suffix); + for (let i = 1; i < keys.length; i++) { + this.appendElementsForType_( + keys[i] + ': ', + object[keys[i]], + indent + label.length + 1, depth + 1, maxDepth, + i < keys.length - 1 ? ',' : '}' + suffix); + } + }, + + appendElementWithLabel_(label, indent, dataElement, suffix) { + const row = document.createElement('div'); + + const indentSpan = document.createElement('span'); + indentSpan.style.whiteSpace = 'pre'; + for (let i = 0; i < indent; i++) { + Polymer.dom(indentSpan).textContent += ' '; + } + Polymer.dom(row).appendChild(indentSpan); + + const labelSpan = document.createElement('span'); + Polymer.dom(labelSpan).textContent = label; + Polymer.dom(row).appendChild(labelSpan); + + Polymer.dom(row).appendChild(dataElement); + const suffixSpan = document.createElement('span'); + Polymer.dom(suffixSpan).textContent = suffix; + Polymer.dom(row).appendChild(suffixSpan); + + row.dataElement = dataElement; + Polymer.dom(this.$.content).appendChild(row); + }, + + appendSimpleText_(label, indent, text, suffix) { + const el = this.ownerDocument.createElement('span'); + Polymer.dom(el).textContent = text; + this.appendElementWithLabel_(label, indent, el, suffix); + return el; + } +}); +</script> + +<dom-module id='tr-ui-a-generic-object-view-with-label'> + <template> + <style> + :host { + display: block; + } + </style> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-generic-object-view-with-label', + + ready() { + this.labelEl_ = document.createElement('div'); + this.genericObjectView_ = + document.createElement('tr-ui-a-generic-object-view'); + Polymer.dom(this.root).appendChild(this.labelEl_); + Polymer.dom(this.root).appendChild(this.genericObjectView_); + }, + + get label() { + return Polymer.dom(this.labelEl_).textContent; + }, + + set label(label) { + Polymer.dom(this.labelEl_).textContent = label; + }, + + get object() { + return this.genericObjectView_.object; + }, + + set object(object) { + this.genericObjectView_.object = object; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view_test.html new file mode 100644 index 00000000000..2e7812e2730 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view_test.html @@ -0,0 +1,222 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/model/object_instance.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('undefinedValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = undefined; + assert.strictEqual(Polymer.dom(view.$.content).textContent, 'undefined'); + }); + + test('nullValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = null; + assert.strictEqual(Polymer.dom(view.$.content).textContent, 'null'); + }); + + test('stringValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = 'string value'; + assert.strictEqual( + Polymer.dom(view.$.content).textContent, '"string value"'); + }); + + test('multiLineStringValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = 'i am a\n string value\ni have\n various indents'; + this.addHTMLOutput(view); + const c = view.$.content; + }); + + test('multiLineStringValueInsideObject', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = {key: 'i am a\n string value\ni have\n various indents', + value: 'simple'}; + this.addHTMLOutput(view); + const c = view.$.content; + }); + + test('jsonObjectStringValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = '{"x": 1}'; + assert.strictEqual(view.$.content.children.length, 1); + assert.strictEqual(view.$.content.children[0].children.length, 4); + }); + + test('jsonArrayStringValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = '[1,2,3]'; + assert.strictEqual(view.$.content.children.length, 3); + }); + + test('booleanValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = false; + assert.strictEqual(Polymer.dom(view.$.content).textContent, 'false'); + }); + + test('numberValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = 3.14159; + assert.strictEqual(Polymer.dom(view.$.content).textContent, '3.14159'); + }); + + test('objectSnapshotValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + + const i10 = new tr.model.ObjectInstance( + {}, '0x1000', 'cat', 'name', 10); + const s10 = i10.addSnapshot(10, {foo: 1}); + + view.object = s10; + this.addHTMLOutput(view); + assert.strictEqual(view.$.content.children[0].dataElement.tagName, + 'TR-UI-A-ANALYSIS-LINK'); + }); + + test('objectInstanceValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + + const i10 = new tr.model.ObjectInstance( + {}, '0x1000', 'cat', 'name', 10); + const s10 = i10.addSnapshot(10, {foo: 1}); + + view.object = i10; + assert.strictEqual(view.$.content.children[0].dataElement.tagName, + 'TR-UI-A-ANALYSIS-LINK'); + }); + + test('instantiate_emptyArrayValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = []; + this.addHTMLOutput(view); + }); + + test('instantiate_twoValueArrayValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = [1, 2]; + this.addHTMLOutput(view); + }); + + test('instantiate_twoValueBArrayValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = [1, {x: 1}]; + this.addHTMLOutput(view); + }); + + test('instantiate_arrayValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = [1, 2, 'three']; + this.addHTMLOutput(view); + }); + + test('instantiate_arrayWithSimpleObjectValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = [{simple: 'object'}]; + this.addHTMLOutput(view); + }); + + test('instantiate_arrayWithComplexObjectValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = [{col0: 'object', col1: 0}, + {col2: 'Object', col3: 1}]; + this.addHTMLOutput(view); + assert.strictEqual(undefined, tr.ui.b.findDeepElementMatching( + view.$.content, 'table')); + }); + + test('instantiate_arrayWithDeepObjectValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = [{key: {deep: 'object values make isTable() return false'}}]; + this.addHTMLOutput(view); + assert.strictEqual(undefined, tr.ui.b.findDeepElementMatching( + view.$.content, 'table')); + }); + + test('jsonTableValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = [ + {col0: 'object', col1: 0, col2: 'foo'}, + {col0: 'Object', col1: 1, col2: 42} + ]; + this.addHTMLOutput(view); + + const table = tr.ui.b.findDeepElementMatching( + view.$.content, 'tr-ui-b-table'); + assert.strictEqual('col0', table.tableColumns[0].title); + assert.strictEqual('col1', table.tableColumns[1].title); + assert.strictEqual( + 'object', table.tableColumns[0].value(table.tableRows[0])); + assert.strictEqual( + 'Object', table.tableColumns[0].value(table.tableRows[1])); + assert.strictEqual(0, table.tableColumns[1].value(table.tableRows[0])); + assert.strictEqual(1, table.tableColumns[1].value(table.tableRows[1])); + assert.isDefined(table.tableColumns[0].cmp); + assert.isDefined(table.tableColumns[1].cmp); + assert.isUndefined(table.tableColumns[2].cmp); + }); + + test('instantiate_objectValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = { + 'entry_one': 'entry_one_value', + 'entry_two': 2, + 'entry_three': [3, 4, 5] + }; + this.addHTMLOutput(view); + }); + + test('timeDurationValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = + new tr.b.Scalar(tr.b.Unit.byName.timeDurationInMs, 3); + this.addHTMLOutput(view); + assert.isDefined(tr.ui.b.findDeepElementMatching( + view.$.content, 'tr-v-ui-scalar-span')); + }); + + test('timeStampValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = new tr.b.Scalar(tr.b.Unit.byName.timeStampInMs, 3); + this.addHTMLOutput(view); + assert.isDefined(tr.ui.b.findDeepElementMatching( + view.$.content, 'tr-v-ui-scalar-span')); + }); + + test('scalarValue', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + view.object = + new tr.b.Scalar(tr.b.Unit.byName.normalizedPercentage, .3); + this.addHTMLOutput(view); + const m = tr.ui.b.findDeepElementMatching( + view.$.content, 'tr-v-ui-scalar-span'); + assert.isDefined(m); + assert.strictEqual(m.value, .3); + assert.strictEqual(m.unit, tr.b.Unit.byName.normalizedPercentage); + }); + + test('httpLink', function() { + const view = document.createElement('tr-ui-a-generic-object-view'); + const url = 'https://google.com/chrome'; + view.object = {a: url}; + this.addHTMLOutput(view); + const a = tr.ui.b.findDeepElementMatching(view.$.content, 'a'); + assert.isDefined(a); + assert.strictEqual(url, a.href); + assert.strictEqual(url, a.textContent); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane.html new file mode 100644 index 00000000000..88b3c3ccc7c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane.html @@ -0,0 +1,893 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/memory_allocator_dump.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_pane.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> +<link rel="import" href="/tracing/ui/analysis/stacked_pane.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + + +<dom-module id='tr-ui-a-memory-dump-allocator-details-pane'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + + #label { + flex: 0 0 auto; + padding: 8px; + + background-color: #eee; + border-bottom: 1px solid #8e8e8e; + border-top: 1px solid white; + + font-size: 15px; + font-weight: bold; + } + + #contents { + flex: 1 0 auto; + align-self: stretch; + font-size: 12px; + } + + #info_text { + padding: 8px; + color: #666; + font-style: italic; + text-align: center; + } + + #table { + display: none; /* Hide until memory allocator dumps are set. */ + flex: 1 0 auto; + align-self: stretch; + font-size: 12px; + } + </style> + <div id="label">Component details</div> + <div id="contents"> + <div id="info_text">No memory allocator dump selected</div> + <tr-ui-b-table id="table"></tr-ui-b-table> + </div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + // Link to docs. + const URL_TO_SIZE_VS_EFFECTIVE_SIZE = 'https://chromium.googlesource.com/chromium/src/+/master/docs/memory-infra/README.md#effective_size-vs_size'; + + // Constant representing the context in suballocation rows. + const SUBALLOCATION_CONTEXT = true; + + // Size numeric info types. + const MemoryAllocatorDumpInfoType = tr.model.MemoryAllocatorDumpInfoType; + const PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN = + MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN; + const PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER = + MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER; + + // Unicode symbols used for memory cell info icons and messages. + const LEFTWARDS_OPEN_HEADED_ARROW = String.fromCharCode(0x21FD); + const RIGHTWARDS_OPEN_HEADED_ARROW = String.fromCharCode(0x21FE); + const EN_DASH = String.fromCharCode(0x2013); + const CIRCLED_LATIN_SMALL_LETTER_I = String.fromCharCode(0x24D8); + + /** @constructor */ + function AllocatorDumpNameColumn() { + tr.ui.analysis.TitleColumn.call(this, 'Component'); + } + + AllocatorDumpNameColumn.prototype = { + __proto__: tr.ui.analysis.TitleColumn.prototype, + + formatTitle(row) { + if (!row.suballocation) { + return row.title; + } + return tr.ui.b.createSpan({ + textContent: row.title, + italic: true, + tooltip: row.fullNames === undefined ? + undefined : row.fullNames.join(', ') + }); + } + }; + + /** + * Retrieve the entry associated with a given name from a map and increment + * its count. + * + * If there is no entry associated with the name, a new entry is created, the + * creation callback is called, the entry's count is incremented (from 0 to + * 1) and the newly created entry is returned. + */ + function getAndUpdateEntry(map, name, createdCallback) { + let entry = map.get(name); + if (entry === undefined) { + entry = {count: 0}; + createdCallback(entry); + map.set(name, entry); + } + entry.count++; + return entry; + } + + /** + * Helper class for building size and effective size column info messages. + * + * @constructor + */ + function SizeInfoMessageBuilder() { + this.parts_ = []; + this.indent_ = 0; + } + + SizeInfoMessageBuilder.prototype = { + append(/* arguments */) { + this.parts_.push.apply( + this.parts_, Array.prototype.slice.apply(arguments)); + }, + + /** + * Append the entries of a map to the message according to the following + * rules: + * + * 1. If the map is empty, append emptyText to the message (if provided). + * Examples: + * + * emptyText=undefined + * Hello, World! ====================> Hello, World! + * + * emptyText='empty' + * The bottle is ====================> The bottle is empty + * + * 2. If the map contains a single entry, append a space and call + * itemCallback on the entry (which is in turn expected to append a + * message for the entry). Example: + * + * Please do not ====================> Please do not [item-message] + * + * 3. If the map contains multiple entries, append them as a list + * with itemCallback called on each entry. If hasPluralSuffix is true, + * 's' will be appended to the message before the list. Examples: + * + * hasPluralSuffix=false + * I need to buy ====================> I need to buy: + * - [item1-message] + * - [item2-message] + * [...] + * - [itemN-message] + * + * hasPluralSuffix=true + * Suspected CL ====================> Suspected CLs: + * - [item1-message] + * - [item2-message] + * [...] + * - [itemN-message] + */ + appendMap( + map, hasPluralSuffix, emptyText, itemCallback, opt_this) { + opt_this = opt_this || this; + if (map.size === 0) { + if (emptyText) { + this.append(emptyText); + } + } else if (map.size === 1) { + this.parts_.push(' '); + const key = map.keys().next().value; + itemCallback.call(opt_this, key, map.get(key)); + } else { + if (hasPluralSuffix) { + this.parts_.push('s'); + } + this.parts_.push(':'); + this.indent_++; + for (const key of map.keys()) { + this.parts_.push('\n', ' '.repeat(3 * (this.indent_ - 1)), ' - '); + itemCallback.call(opt_this, key, map.get(key)); + } + this.indent_--; + } + }, + + appendImportanceRange(range) { + this.append(' (importance: '); + if (range.min === range.max) { + this.append(range.min); + } else { + this.append(range.min, EN_DASH, range.max); + } + this.append(')'); + }, + + appendSizeIfDefined(size) { + if (size !== undefined) { + this.append(' (', tr.b.Unit.byName.sizeInBytes.format(size), ')'); + } + }, + + appendSomeTimestampsQuantifier() { + this.append( + ' ', tr.ui.analysis.MemoryColumn.SOME_TIMESTAMPS_INFO_QUANTIFIER); + }, + + build() { + return this.parts_.join(''); + } + }; + + /** @constructor */ + function EffectiveSizeColumn(name, cellPath, aggregationMode) { + tr.ui.analysis.DetailsNumericMemoryColumn.call( + this, name, cellPath, aggregationMode); + } + + EffectiveSizeColumn.prototype = { + __proto__: tr.ui.analysis.DetailsNumericMemoryColumn.prototype, + + get title() { + return tr.ui.b.createLink({ + textContent: this.name, + tooltip: 'Memory used by this component', + href: URL_TO_SIZE_VS_EFFECTIVE_SIZE + }); + }, + + addInfos(numerics, memoryAllocatorDumps, infos) { + if (memoryAllocatorDumps === undefined) return; + + // Quantified name of an owner dump (of the given dump) -> {count, + // importanceRange}. + const ownerNameToEntry = new Map(); + + // Quantified name of an owned dump (by the given dump) -> {count, + // importanceRange, sharerNameToEntry}, where sharerNameToEntry is a map + // from quantified names of other owners of the owned dump to {count, + // importanceRange}. + const ownedNameToEntry = new Map(); + + for (let i = 0; i < numerics.length; i++) { + if (numerics[i] === undefined) continue; + + const dump = memoryAllocatorDumps[i]; + if (dump === SUBALLOCATION_CONTEXT) { + return; // No ownership of suballocation internal rows. + } + + // Gather owners of this dump. + dump.ownedBy.forEach(function(ownerLink) { + const ownerDump = ownerLink.source; + this.getAndUpdateOwnershipEntry_( + ownerNameToEntry, ownerDump, ownerLink); + }, this); + + // Gather dumps owned by this dump and other owner dumps sharing them + // (with this dump). + const ownedLink = dump.owns; + if (ownedLink !== undefined) { + const ownedDump = ownedLink.target; + const ownedEntry = this.getAndUpdateOwnershipEntry_(ownedNameToEntry, + ownedDump, ownedLink, true /* opt_withSharerNameToEntry */); + const sharerNameToEntry = ownedEntry.sharerNameToEntry; + ownedDump.ownedBy.forEach(function(sharerLink) { + const sharerDump = sharerLink.source; + if (sharerDump === dump) return; + this.getAndUpdateOwnershipEntry_( + sharerNameToEntry, sharerDump, sharerLink); + }, this); + } + } + + // Emit a single info listing all owners of this dump. + if (ownerNameToEntry.size > 0) { + const messageBuilder = new SizeInfoMessageBuilder(); + messageBuilder.append('shared by'); + messageBuilder.appendMap( + ownerNameToEntry, + false /* hasPluralSuffix */, + undefined /* emptyText */, + function(ownerName, ownerEntry) { + messageBuilder.append(ownerName); + if (ownerEntry.count < numerics.length) { + messageBuilder.appendSomeTimestampsQuantifier(); + } + messageBuilder.appendImportanceRange(ownerEntry.importanceRange); + }, this); + infos.push({ + message: messageBuilder.build(), + icon: LEFTWARDS_OPEN_HEADED_ARROW, + color: 'green' + }); + } + + // Emit a single info listing all dumps owned by this dump together + // with list(s) of other owner dumps sharing them with this dump. + if (ownedNameToEntry.size > 0) { + const messageBuilder = new SizeInfoMessageBuilder(); + messageBuilder.append('shares'); + messageBuilder.appendMap( + ownedNameToEntry, + false /* hasPluralSuffix */, + undefined /* emptyText */, + function(ownedName, ownedEntry) { + messageBuilder.append(ownedName); + const ownedCount = ownedEntry.count; + if (ownedCount < numerics.length) { + messageBuilder.appendSomeTimestampsQuantifier(); + } + messageBuilder.appendImportanceRange(ownedEntry.importanceRange); + messageBuilder.append(' with'); + messageBuilder.appendMap( + ownedEntry.sharerNameToEntry, + false /* hasPluralSuffix */, + ' no other dumps', + function(sharerName, sharerEntry) { + messageBuilder.append(sharerName); + if (sharerEntry.count < ownedCount) { + messageBuilder.appendSomeTimestampsQuantifier(); + } + messageBuilder.appendImportanceRange( + sharerEntry.importanceRange); + }, this); + }, this); + infos.push({ + message: messageBuilder.build(), + icon: RIGHTWARDS_OPEN_HEADED_ARROW, + color: 'green' + }); + } + }, + + getAndUpdateOwnershipEntry_( + map, dump, link, opt_withSharerNameToEntry) { + const entry = getAndUpdateEntry(map, dump.quantifiedName, + function(newEntry) { + newEntry.importanceRange = new tr.b.math.Range(); + if (opt_withSharerNameToEntry) { + newEntry.sharerNameToEntry = new Map(); + } + }); + entry.importanceRange.addValue(link.importance || 0); + return entry; + } + }; + + /** @constructor */ + function SizeColumn(name, cellPath, aggregationMode) { + tr.ui.analysis.DetailsNumericMemoryColumn.call( + this, name, cellPath, aggregationMode); + } + + SizeColumn.prototype = { + __proto__: tr.ui.analysis.DetailsNumericMemoryColumn.prototype, + + get title() { + return tr.ui.b.createLink({ + textContent: this.name, + tooltip: 'Memory requested by this component', + href: URL_TO_SIZE_VS_EFFECTIVE_SIZE + }); + }, + + addInfos(numerics, memoryAllocatorDumps, infos) { + if (memoryAllocatorDumps === undefined) return; + this.addOverlapInfo_(numerics, memoryAllocatorDumps, infos); + this.addProvidedSizeWarningInfos_(numerics, memoryAllocatorDumps, infos); + }, + + addOverlapInfo_(numerics, memoryAllocatorDumps, infos) { + // Sibling allocator dump name -> {count, size}. The latter field (size) + // is omitted in multi-selection mode. + const siblingNameToEntry = new Map(); + for (let i = 0; i < numerics.length; i++) { + if (numerics[i] === undefined) continue; + const dump = memoryAllocatorDumps[i]; + if (dump === SUBALLOCATION_CONTEXT) { + return; // No ownership of suballocation internal rows. + } + const ownedBySiblingSizes = dump.ownedBySiblingSizes; + for (const siblingDump of ownedBySiblingSizes.keys()) { + const siblingName = siblingDump.name; + getAndUpdateEntry(siblingNameToEntry, siblingName, + function(newEntry) { + if (numerics.length === 1 /* single-selection mode */) { + newEntry.size = ownedBySiblingSizes.get(siblingDump); + } + }); + } + } + + // Emit a single info describing all overlaps with siblings (if + // applicable). + if (siblingNameToEntry.size > 0) { + const messageBuilder = new SizeInfoMessageBuilder(); + messageBuilder.append('overlaps with its sibling'); + messageBuilder.appendMap( + siblingNameToEntry, + true /* hasPluralSuffix */, + undefined /* emptyText */, + function(siblingName, siblingEntry) { + messageBuilder.append('\'', siblingName, '\''); + messageBuilder.appendSizeIfDefined(siblingEntry.size); + if (siblingEntry.count < numerics.length) { + messageBuilder.appendSomeTimestampsQuantifier(); + } + }, this); + infos.push({ + message: messageBuilder.build(), + icon: CIRCLED_LATIN_SMALL_LETTER_I, + color: 'blue' + }); + } + }, + + addProvidedSizeWarningInfos_(numerics, memoryAllocatorDumps, + infos) { + // Info type (see MemoryAllocatorDumpInfoType) -> {count, providedSize, + // dependencySize}. The latter two fields (providedSize and + // dependencySize) are omitted in multi-selection mode. + const infoTypeToEntry = new Map(); + for (let i = 0; i < numerics.length; i++) { + if (numerics[i] === undefined) continue; + const dump = memoryAllocatorDumps[i]; + if (dump === SUBALLOCATION_CONTEXT) { + return; // Suballocation internal rows have no provided size. + } + dump.infos.forEach(function(dumpInfo) { + getAndUpdateEntry(infoTypeToEntry, dumpInfo.type, function(newEntry) { + if (numerics.length === 1 /* single-selection mode */) { + newEntry.providedSize = dumpInfo.providedSize; + newEntry.dependencySize = dumpInfo.dependencySize; + } + }); + }); + } + + // Emit a warning info for every info type. + for (const infoType of infoTypeToEntry.keys()) { + const entry = infoTypeToEntry.get(infoType); + const messageBuilder = new SizeInfoMessageBuilder(); + messageBuilder.append('provided size'); + messageBuilder.appendSizeIfDefined(entry.providedSize); + let dependencyName; + switch (infoType) { + case PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN: + dependencyName = 'the aggregated size of the children'; + break; + case PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER: + dependencyName = 'the size of the largest owner'; + break; + default: + dependencyName = 'an unknown dependency'; + break; + } + messageBuilder.append(' was less than ', dependencyName); + messageBuilder.appendSizeIfDefined(entry.dependencySize); + if (entry.count < numerics.length) { + messageBuilder.appendSomeTimestampsQuantifier(); + } + infos.push(tr.ui.analysis.createWarningInfo(messageBuilder.build())); + } + } + }; + + const NUMERIC_COLUMN_RULES = [ + { + condition: tr.model.MemoryAllocatorDump.EFFECTIVE_SIZE_NUMERIC_NAME, + importance: 10, + columnConstructor: EffectiveSizeColumn + }, + { + condition: tr.model.MemoryAllocatorDump.SIZE_NUMERIC_NAME, + importance: 9, + columnConstructor: SizeColumn + }, + { + condition: 'page_size', + importance: 0, + columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn + }, + { + condition: /size/, + importance: 5, + columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn + }, + { + // All other columns. + importance: 0, + columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn + } + ]; + + const DIAGNOSTIC_COLUMN_RULES = [ + { + importance: 0, + columnConstructor: tr.ui.analysis.StringMemoryColumn + } + ]; + + Polymer({ + is: 'tr-ui-a-memory-dump-allocator-details-pane', + behaviors: [tr.ui.analysis.StackedPane], + + created() { + this.memoryAllocatorDumps_ = undefined; + this.heapDumps_ = undefined; + this.aggregationMode_ = undefined; + }, + + ready() { + this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + }, + + /** + * Sets the memory allocator dumps and schedules rebuilding the pane. + * + * The provided value should be a chronological list of memory allocator + * dumps. All dumps are assumed to belong to the same process and have + * the same full name. Example: + * + * [ + * tr.model.MemoryAllocatorDump {}, // MAD at timestamp 1. + * undefined, // MAD not provided at timestamp 2. + * tr.model.MemoryAllocatorDump {}, // MAD at timestamp 3. + * ] + */ + set memoryAllocatorDumps(memoryAllocatorDumps) { + this.memoryAllocatorDumps_ = memoryAllocatorDumps; + this.scheduleRebuild_(); + }, + + get memoryAllocatorDumps() { + return this.memoryAllocatorDumps_; + }, + + // TODO(petrcermak): Don't plumb the heap dumps through the allocator + // details pane. Maybe add support for multiple child panes to stacked pane + // (view) instead. + set heapDumps(heapDumps) { + this.heapDumps_ = heapDumps; + this.scheduleRebuild_(); + }, + + set aggregationMode(aggregationMode) { + this.aggregationMode_ = aggregationMode; + this.scheduleRebuild_(); + }, + + get aggregationMode() { + return this.aggregationMode_; + }, + + onRebuild_() { + if (this.memoryAllocatorDumps_ === undefined || + this.memoryAllocatorDumps_.length === 0) { + // Show the info text (hide the table). + this.$.info_text.style.display = 'block'; + this.$.table.style.display = 'none'; + + this.$.table.clear(); + this.$.table.rebuild(); + + // Hide the heap details pane (if applicable). + this.childPaneBuilder = undefined; + return; + } + + // Show the table (hide the info text). + this.$.info_text.style.display = 'none'; + this.$.table.style.display = 'block'; + + const rows = this.createRows_(); + const columns = this.createColumns_(rows); + rows.forEach(function(rootRow) { + tr.ui.analysis.aggregateTableRowCellsRecursively(rootRow, columns, + function(contexts) { + // Only aggregate suballocation rows (numerics of regular rows + // corresponding to MADs have already been aggregated by the + // model in MemoryAllocatorDump.aggregateNumericsRecursively). + return contexts !== undefined && contexts.some(function(context) { + return context === SUBALLOCATION_CONTEXT; + }); + }); + }); + + this.$.table.tableRows = rows; + this.$.table.tableColumns = columns; + this.$.table.rebuild(); + tr.ui.analysis.expandTableRowsRecursively(this.$.table); + + // Show/hide the heap details pane. + if (this.heapDumps_ === undefined) { + this.childPaneBuilder = undefined; + } else { + this.childPaneBuilder = function() { + const pane = + document.createElement('tr-ui-a-memory-dump-heap-details-pane'); + pane.heapDumps = this.heapDumps_; + pane.aggregationMode = this.aggregationMode_; + return pane; + }.bind(this); + } + }, + + createRows_() { + return [ + this.createAllocatorRowRecursively_(this.memoryAllocatorDumps_) + ]; + }, + + createAllocatorRowRecursively_(dumps) { + // Get the name of the memory allocator dumps. We can use any defined + // dump in dumps since they all have the same name. + const definedDump = dumps.find(x => x); + const title = definedDump.name; + const fullName = definedDump.fullName; + + // Transform a chronological list of memory allocator dumps into two + // dictionaries of cells (where each cell contains a chronological list + // of the values of one of its numerics or diagnostics). + const numericCells = tr.ui.analysis.createCells(dumps, function(dump) { + return dump.numerics; + }); + const diagnosticCells = tr.ui.analysis.createCells(dumps, function(dump) { + return dump.diagnostics; + }); + + // Determine whether the memory allocator dump is a suballocation. A + // dump is assumed to be a suballocation if (1) its name starts with + // two underscores, (2) it has an owner from within the same process at + // some timestamp, and (3) it is undefined, has no owners, or has the + // same owner (and no other owners) at all other timestamps. + let suballocatedBy = undefined; + if (title.startsWith('__')) { + for (let i = 0; i < dumps.length; i++) { + const dump = dumps[i]; + if (dump === undefined || dump.ownedBy.length === 0) { + // Ignore timestamps where the dump is undefined or doesn't + // have any owner. + continue; + } + const ownerDump = dump.ownedBy[0].source; + if (dump.ownedBy.length > 1 || + dump.children.length > 0 || + ownerDump.containerMemoryDump !== dump.containerMemoryDump) { + // If the dump has (1) any children, (2) multiple owners, or + // (3) its owner is in a different process (otherwise, the + // modified title would be ambiguous), then it's not considered + // to be a suballocation. + suballocatedBy = undefined; + break; + } + if (suballocatedBy === undefined) { + suballocatedBy = ownerDump.fullName; + } else if (suballocatedBy !== ownerDump.fullName) { + // The full name of the owner dump changed over time, so this + // dump is not a suballocation. + suballocatedBy = undefined; + break; + } + } + } + + const row = { + title, + fullNames: [fullName], + contexts: dumps, + numericCells, + diagnosticCells, + suballocatedBy + }; + + // Child memory dump name (dict key) -> Timestamp (list index) -> + // Child dump. + const childDumpNameToDumps = tr.b.invertArrayOfDicts(dumps, + function(dump) { + const results = {}; + for (const child of dump.children) { + results[child.name] = child; + } + return results; + }); + + // Recursively create sub-rows for children (if applicable). + const subRows = []; + let suballocationClassificationRootNode = undefined; + for (const childDumps of Object.values(childDumpNameToDumps)) { + const childRow = this.createAllocatorRowRecursively_(childDumps); + if (childRow.suballocatedBy === undefined) { + // Not a suballocation row: just append it. + subRows.push(childRow); + } else { + // Suballocation row: classify it in a tree of suballocations. + suballocationClassificationRootNode = + this.classifySuballocationRow_( + childRow, suballocationClassificationRootNode); + } + } + + // Build the tree of suballocations (if applicable). + if (suballocationClassificationRootNode !== undefined) { + const suballocationRow = this.createSuballocationRowRecursively_( + 'suballocations', suballocationClassificationRootNode); + subRows.push(suballocationRow); + } + + if (subRows.length > 0) { + row.subRows = subRows; + } + + return row; + }, + + classifySuballocationRow_(suballocationRow, rootNode) { + if (rootNode === undefined) { + rootNode = { + children: {}, + row: undefined + }; + } + + const suballocationLevels = suballocationRow.suballocatedBy.split('/'); + let currentNode = rootNode; + for (let i = 0; i < suballocationLevels.length; i++) { + const suballocationLevel = suballocationLevels[i]; + let nextNode = currentNode.children[suballocationLevel]; + if (nextNode === undefined) { + currentNode.children[suballocationLevel] = nextNode = { + children: {}, + row: undefined + }; + } + currentNode = nextNode; + } + + const existingRow = currentNode.row; + if (existingRow !== undefined) { + // On rare occasions it can happen that one dump (e.g. sqlite) owns + // different suballocations at different timestamps (e.g. + // malloc/allocated_objects/_7d35 and malloc/allocated_objects/_511e). + // When this happens, we merge the two suballocations into a single row + // (malloc/allocated_objects/suballocations/sqlite). + for (let i = 0; i < suballocationRow.contexts.length; i++) { + const newContext = suballocationRow.contexts[i]; + if (newContext === undefined) continue; + + if (existingRow.contexts[i] !== undefined) { + throw new Error('Multiple suballocations with the same owner name'); + } + + existingRow.contexts[i] = newContext; + ['numericCells', 'diagnosticCells'].forEach(function(cellKey) { + const suballocationCells = suballocationRow[cellKey]; + if (suballocationCells === undefined) return; + for (const [cellName, cell] of Object.entries(suballocationCells)) { + if (cell === undefined) continue; + const fields = cell.fields; + if (fields === undefined) continue; + const field = fields[i]; + if (field === undefined) continue; + let existingCells = existingRow[cellKey]; + if (existingCells === undefined) { + existingCells = {}; + existingRow[cellKey] = existingCells; + } + let existingCell = existingCells[cellName]; + if (existingCell === undefined) { + existingCell = new tr.ui.analysis.MemoryCell( + new Array(fields.length)); + existingCells[cellName] = existingCell; + } + existingCell.fields[i] = field; + } + }); + } + existingRow.fullNames.push.apply( + existingRow.fullNames, suballocationRow.fullNames); + } else { + currentNode.row = suballocationRow; + } + + return rootNode; + }, + + createSuballocationRowRecursively_(name, node) { + const childCount = Object.keys(node.children).length; + if (childCount === 0) { + if (node.row === undefined) { + throw new Error('Suballocation node must have a row or children'); + } + // Leaf row of the suballocation tree: Change the row's title from + // '__MEANINGLESSHASH' to the name of the suballocation owner. + const row = node.row; + row.title = name; + row.suballocation = true; + return row; + } + + // Internal row of the suballocation tree: Recursively create its + // sub-rows. + const subRows = []; + for (const [subName, subNode] of Object.entries(node.children)) { + subRows.push(this.createSuballocationRowRecursively_(subName, subNode)); + } + + if (node.row !== undefined) { + // Very unlikely case: Both an ancestor (e.g. 'skia') and one of its + // descendants (e.g. 'skia/sk_glyph_cache') both suballocate from the + // same MemoryAllocatorDump (e.g. 'malloc/allocated_objects'). In + // this case, the suballocation from the ancestor must be mapped to + // 'malloc/allocated_objects/suballocations/skia/<unspecified>' so + // that 'malloc/allocated_objects/suballocations/skia' could + // aggregate the numerics of the two suballocations properly. + const row = node.row; + row.title = '<unspecified>'; + row.suballocation = true; + subRows.unshift(row); + } + + // An internal row of the suballocation tree is assumed to be defined + // at a given timestamp if at least one of its sub-rows is defined at + // the timestamp. + const contexts = new Array(subRows[0].contexts.length); + for (let i = 0; i < subRows.length; i++) { + subRows[i].contexts.forEach(function(subContext, index) { + if (subContext !== undefined) { + contexts[index] = SUBALLOCATION_CONTEXT; + } + }); + } + + return { + title: name, + suballocation: true, + contexts, + subRows + }; + }, + + createColumns_(rows) { + const titleColumn = new AllocatorDumpNameColumn(); + titleColumn.width = '200px'; + + const numericColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, { + cellKey: 'numericCells', + aggregationMode: this.aggregationMode_, + rules: NUMERIC_COLUMN_RULES + }); + const diagnosticColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, { + cellKey: 'diagnosticCells', + aggregationMode: this.aggregationMode_, + rules: DIAGNOSTIC_COLUMN_RULES + }); + const fieldColumns = numericColumns.concat(diagnosticColumns); + tr.ui.analysis.MemoryColumn.spaceEqually(fieldColumns); + + const columns = [titleColumn].concat(fieldColumns); + return columns; + } + }); + + return { + // All exports are for testing only. + SUBALLOCATION_CONTEXT, + AllocatorDumpNameColumn, + EffectiveSizeColumn, + SizeColumn, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane_test.html new file mode 100644 index 00000000000..6fda765b34b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane_test.html @@ -0,0 +1,1261 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/heap_dump.html"> +<link rel="import" href="/tracing/model/memory_allocator_dump.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_allocator_details_pane.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const MemoryAllocatorDump = tr.model.MemoryAllocatorDump; + const Scalar = tr.b.Scalar; + const unitlessNumber_smallerIsBetter = + tr.b.Unit.byName.unitlessNumber_smallerIsBetter; + const sizeInBytes_smallerIsBetter = + tr.b.Unit.byName.sizeInBytes_smallerIsBetter; + const HeapDump = tr.model.HeapDump; + const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode; + const checkNumericFields = tr.ui.analysis.checkNumericFields; + const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields; + const checkStringFields = tr.ui.analysis.checkStringFields; + const checkColumnInfosAndColor = tr.ui.analysis.checkColumnInfosAndColor; + const checkColumns = tr.ui.analysis.checkColumns; + const isElementDisplayed = tr.ui.analysis.isElementDisplayed; + const AllocatorDumpNameColumn = tr.ui.analysis.AllocatorDumpNameColumn; + const EffectiveSizeColumn = tr.ui.analysis.EffectiveSizeColumn; + const SizeColumn = tr.ui.analysis.SizeColumn; + const StringMemoryColumn = tr.ui.analysis.StringMemoryColumn; + const NumericMemoryColumn = tr.ui.analysis.NumericMemoryColumn; + const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump; + const addProcessMemoryDump = + tr.model.MemoryDumpTestUtils.addProcessMemoryDump; + const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump; + const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump; + const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink; + + const SUBALLOCATION_CONTEXT = tr.ui.analysis.SUBALLOCATION_CONTEXT; + const MemoryAllocatorDumpInfoType = tr.model.MemoryAllocatorDumpInfoType; + const PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN = + MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN; + const PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER = + MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER; + + function addRootDumps(containerMemoryDump, rootNames, addedCallback) { + // Test sanity check. + assert.isUndefined(containerMemoryDump.memoryAllocatorDumps); + + const rootDumps = rootNames.map(function(rootName) { + return new MemoryAllocatorDump(containerMemoryDump, rootName); + }); + addedCallback.apply(null, rootDumps); + containerMemoryDump.memoryAllocatorDumps = rootDumps; + } + + function newSuballocationDump(ownerDump, parentDump, name, size) { + const suballocationDump = addChildDump(parentDump, name, + {numerics: {size}}); + if (ownerDump !== undefined) { + addOwnershipLink(ownerDump, suballocationDump); + } + return suballocationDump; + } + + function createProcessMemoryDumps() { + const model = tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + + // First timestamp. + const gmd1 = addGlobalMemoryDump(model, {ts: -10}); + const pmd1 = addProcessMemoryDump(gmd1, process, {ts: -11}); + pmd1.memoryAllocatorDumps = (function() { + const v8Dump = newAllocatorDump(pmd1, 'v8', {numerics: { + size: 1073741824 /* 1 GiB */, + inner_size: 2097152 /* 2 MiB */, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 204) + }}); + + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 805306368 /* 768 MiB */}}); + addChildDump(v8HeapsDump, 'heap42', + {numerics: {size: 804782080 /* 767.5 MiB */}}); + + const v8ObjectsDump = addChildDump(v8Dump, 'objects'); + v8ObjectsDump.addDiagnostic('url', 'http://example.com'); + addChildDump(v8ObjectsDump, 'foo', + {numerics: {size: 1022976 /* 999 KiB */}}); + addChildDump(v8ObjectsDump, 'bar', + {numerics: {size: 1024000 /* 1000 KiB */}}); + + const oilpanDump = newAllocatorDump(pmd1, 'oilpan', + {numerics: {size: 125829120 /* 120 MiB */}}); + newSuballocationDump( + oilpanDump, v8Dump, '__99BEAD', 150994944 /* 144 MiB */); + + const oilpanSubDump = addChildDump(oilpanDump, 'animals'); + + const oilpanSubDump1 = addChildDump(oilpanSubDump, 'cow', + {numerics: {size: 33554432 /* 32 MiB */}}); + newSuballocationDump( + oilpanSubDump1, v8Dump, '__42BEEF', 67108864 /* 64 MiB */); + + const oilpanSubDump2 = addChildDump(oilpanSubDump, 'chicken', + {numerics: {size: 16777216 /* 16 MiB */}}); + newSuballocationDump( + oilpanSubDump2, v8Dump, '__68DEAD', 33554432 /* 32 MiB */); + + const skiaDump = newAllocatorDump(pmd1, 'skia', + {numerics: {size: 8388608 /* 8 MiB */}}); + const suballocationDump = newSuballocationDump( + skiaDump, v8Dump, '__15FADE', 16777216 /* 16 MiB */); + + const ccDump = newAllocatorDump(pmd1, 'cc', + {numerics: {size: 4194304 /* 4 MiB */}}); + newSuballocationDump( + ccDump, v8Dump, '__12FEED', 5242880 /* 5 MiB */).addDiagnostic( + 'url', 'localhost:1234'); + + return [v8Dump, oilpanDump, skiaDump, ccDump]; + })(); + + // Second timestamp. + const gmd2 = addGlobalMemoryDump(model, {ts: 10}); + const pmd2 = addProcessMemoryDump(gmd2, process, {ts: 11}); + pmd2.memoryAllocatorDumps = (function() { + const v8Dump = newAllocatorDump(pmd2, 'v8', {numerics: { + size: 1073741824 /* 1 GiB */, + inner_size: 2097152 /* 2 MiB */, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 204) + }}); + + const v8ObjectsDump = addChildDump(v8Dump, 'objects'); + v8ObjectsDump.addDiagnostic('url', 'http://sample.net'); + addChildDump(v8ObjectsDump, 'foo', + {numerics: {size: 1020928 /* 997 KiB */}}); + addChildDump(v8ObjectsDump, 'bar', + {numerics: {size: 1026048 /* 1002 KiB */}}); + + newSuballocationDump( + undefined, v8Dump, '__99BEAD', 268435456 /* 256 MiB */); + + const ccDump = newAllocatorDump(pmd2, 'cc', + {numerics: {size: 7340032 /* 7 MiB */}}); + newSuballocationDump( + ccDump, v8Dump, '__13DEED', 11534336 /* 11 MiB */).addDiagnostic( + 'url', 'localhost:5678'); + + return [v8Dump, ccDump]; + })(); + }); + + return model.processes[1].memoryDumps; + } + + function createSizeFields(values) { + return values.map(function(value) { + if (value === undefined) return undefined; + return new Scalar(sizeInBytes_smallerIsBetter, value); + }); + } + + const EXPECTED_COLUMNS = [ + { title: 'Component', type: AllocatorDumpNameColumn, noAggregation: true }, + { title: 'effective_size', type: EffectiveSizeColumn }, + { title: 'size', type: SizeColumn }, + { title: 'inner_size', type: NumericMemoryColumn }, + { title: 'objects_count', type: NumericMemoryColumn }, + { title: 'url', type: StringMemoryColumn } + ]; + + function checkRow(columns, row, expectations) { + const formattedTitle = columns[0].formatTitle(row); + const expectedTitle = expectations.title; + if (typeof expectedTitle === 'function') { + expectedTitle(formattedTitle); + } else { + assert.strictEqual(formattedTitle, expectedTitle); + } + + checkSizeNumericFields(row, columns[1], expectations.size); + checkSizeNumericFields(row, columns[2], expectations.effective_size); + checkSizeNumericFields(row, columns[3], expectations.inner_size); + checkNumericFields(row, columns[4], expectations.objects_count, + unitlessNumber_smallerIsBetter); + checkStringFields(row, columns[5], expectations.url); + + const expectedSubRowCount = expectations.sub_row_count; + if (expectedSubRowCount === undefined) { + assert.isUndefined(row.subRows); + } else { + assert.lengthOf(row.subRows, expectedSubRowCount); + } + + const expectedContexts = expectations.contexts; + if (expectedContexts === undefined) { + assert.isUndefined(row.contexts); + } else { + assert.deepEqual(Array.from(row.contexts), expectedContexts); + } + } + + function buildProcessMemoryDumps(count, preFinalizeDumpsCallback) { + const pmds = new Array(count); + tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + for (let i = 0; i < count; i++) { + const timestamp = 10 + i; + const gmd = addGlobalMemoryDump(model, {ts: timestamp}); + pmds[i] = addProcessMemoryDump(gmd, process, {ts: timestamp}); + } + preFinalizeDumpsCallback(pmds); + }); + return pmds; + } + + function getAllocatorDumps(pmds, fullName) { + return pmds.map(function(pmd) { + if (pmd === undefined) return undefined; + return pmd.getMemoryAllocatorDumpByFullName(fullName); + }); + } + + function checkAllocatorPaneColumnInfosAndColor( + column, dumps, numericName, expectedInfos) { + const numerics = dumps.map(function(dump) { + if (dump === undefined) return undefined; + return dump.numerics[numericName]; + }); + checkColumnInfosAndColor( + column, numerics, dumps, expectedInfos, undefined /* no color */); + } + + test('instantiate_empty', function() { + tr.ui.analysis.createAndCheckEmptyPanes(this, + 'tr-ui-a-memory-dump-allocator-details-pane', 'memoryAllocatorDumps', + function(viewEl) { + // Check that the info text is shown. + assert.isTrue(isElementDisplayed(viewEl.$.info_text)); + assert.isFalse(isElementDisplayed(viewEl.$.table)); + }); + }); + + test('instantiate_single', function() { + const processMemoryDumps = createProcessMemoryDumps().slice(0, 1); + + const viewEl = tr.ui.analysis.createTestPane( + 'tr-ui-a-memory-dump-allocator-details-pane'); + viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8'); + viewEl.rebuild(); + assert.deepEqual(viewEl.requestedChildPanes, [undefined]); + this.addHTMLOutput(viewEl); + + // Check that the table is shown. + assert.isTrue(isElementDisplayed(viewEl.$.table)); + assert.isFalse(isElementDisplayed(viewEl.$.info_text)); + + const table = viewEl.$.table; + const columns = table.tableColumns; + checkColumns(columns, EXPECTED_COLUMNS, undefined /* no aggregation */); + const rows = table.tableRows; + assert.lengthOf(rows, 1); + + // Check the rows of the table. + const rootRow = rows[0]; + checkRow(columns, rootRow, { + title: 'v8', + size: [942619648], + effective_size: [1081031680], + inner_size: [2097152], + objects_count: [204], + sub_row_count: 3, + contexts: getAllocatorDumps(processMemoryDumps, 'v8'), + }); + + const heapsSubRow = rootRow.subRows[0]; + checkRow(columns, heapsSubRow, { + title: 'heaps', + size: [805306368], + effective_size: [805306368], + sub_row_count: 2, + contexts: getAllocatorDumps(processMemoryDumps, 'v8/heaps'), + }); + + const heapsUnspecifiedSubRow = heapsSubRow.subRows[0]; + checkRow(columns, heapsUnspecifiedSubRow, { + title: '<unspecified>', + size: [524288], + effective_size: [524288], + contexts: getAllocatorDumps(processMemoryDumps, 'v8/heaps/<unspecified>'), + }); + + const suballocationsSubRow = rootRow.subRows[2]; + checkRow(columns, suballocationsSubRow, { + title(formattedTitle) { + assert.strictEqual( + Polymer.dom(formattedTitle).textContent, 'suballocations'); + assert.strictEqual(formattedTitle.title, ''); + }, + size: [135266304], + effective_size: [273678336], + sub_row_count: 3, + contexts: [SUBALLOCATION_CONTEXT], + }); + + const oilpanSuballocationSubRow = suballocationsSubRow.subRows[0]; + checkRow(columns, oilpanSuballocationSubRow, { + title(formattedTitle) { + assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'oilpan'); + assert.strictEqual(formattedTitle.title, ''); + }, + size: [125829120], + effective_size: [251658240], + sub_row_count: 2, + contexts: [SUBALLOCATION_CONTEXT], + }); + + const oilpanUnspecifiedSuballocationSubRow = + oilpanSuballocationSubRow.subRows[0]; + checkRow(columns, oilpanUnspecifiedSuballocationSubRow, { + title(formattedTitle) { + assert.strictEqual( + Polymer.dom(formattedTitle).textContent, '<unspecified>'); + assert.strictEqual(formattedTitle.title, 'v8/__99BEAD'); + }, + size: [75497472], + effective_size: [150994944], + contexts: getAllocatorDumps(processMemoryDumps, 'v8/__99BEAD'), + }); + + const oilpanAnimalsSuballocationSubRow = + oilpanSuballocationSubRow.subRows[1]; + checkRow(columns, oilpanAnimalsSuballocationSubRow, { + title(formattedTitle) { + assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'animals'); + assert.strictEqual(formattedTitle.title, ''); + }, + size: [50331648], + effective_size: [100663296], + sub_row_count: 2, + contexts: [SUBALLOCATION_CONTEXT], + }); + + const oilpanCowSuballocationSubRow = + oilpanAnimalsSuballocationSubRow.subRows[0]; + checkRow(columns, oilpanCowSuballocationSubRow, { + title(formattedTitle) { + assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'cow'); + assert.strictEqual(formattedTitle.title, 'v8/__42BEEF'); + }, + size: [33554432], + effective_size: [67108864], + contexts: getAllocatorDumps(processMemoryDumps, 'v8/__42BEEF'), + }); + + const skiaSuballocationSubRow = suballocationsSubRow.subRows[1]; + checkRow(columns, skiaSuballocationSubRow, { + title(formattedTitle) { + assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'skia'); + assert.strictEqual(formattedTitle.title, 'v8/__15FADE'); + }, + size: [8388608], + effective_size: [16777216], + contexts: getAllocatorDumps(processMemoryDumps, 'v8/__15FADE'), + }); + + const ccSuballocationSubRow = suballocationsSubRow.subRows[2]; + checkRow(columns, ccSuballocationSubRow, { + title(formattedTitle) { + assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'cc'); + assert.strictEqual(formattedTitle.title, 'v8/__12FEED'); + }, + size: [1048576], + effective_size: [5242880], + url: ['localhost:1234'], + contexts: getAllocatorDumps(processMemoryDumps, 'v8/__12FEED') + }); + }); + + test('instantiate_multipleDiff', function() { + const processMemoryDumps = createProcessMemoryDumps(); + + const viewEl = tr.ui.analysis.createTestPane( + 'tr-ui-a-memory-dump-allocator-details-pane'); + viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8'); + viewEl.aggregationMode = AggregationMode.DIFF; + viewEl.rebuild(); + assert.deepEqual(viewEl.requestedChildPanes, [undefined]); + this.addHTMLOutput(viewEl); + + // Check that the table is shown. + assert.isTrue(isElementDisplayed(viewEl.$.table)); + assert.isFalse(isElementDisplayed(viewEl.$.info_text)); + + const table = viewEl.$.table; + const columns = table.tableColumns; + checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.DIFF); + const rows = table.tableRows; + assert.lengthOf(rows, 1); + + // Check the rows of the table. + const rootRow = rows[0]; + checkRow(columns, rootRow, { + title: 'v8', + size: [942619648, 1066401792], + effective_size: [1081031680, 1073741824], + inner_size: [2097152, 2097152], + objects_count: [204, 204], + sub_row_count: 4, + contexts: getAllocatorDumps(processMemoryDumps, 'v8'), + }); + + const heapsSubRow = rootRow.subRows[0]; + checkRow(columns, heapsSubRow, { + title: 'heaps', + size: [805306368, undefined], + effective_size: [805306368, undefined], + sub_row_count: 2, + contexts: getAllocatorDumps(processMemoryDumps, 'v8/heaps'), + }); + + const heapsUnspecifiedSubRow = heapsSubRow.subRows[0]; + checkRow(columns, heapsUnspecifiedSubRow, { + title: '<unspecified>', + size: [524288, undefined], + effective_size: [524288, undefined], + contexts: getAllocatorDumps(processMemoryDumps, 'v8/heaps/<unspecified>'), + }); + + const unspecifiedSubRow = rootRow.subRows[2]; + checkRow(columns, unspecifiedSubRow, { + title: '<unspecified>', + size: [undefined, 791725056], + effective_size: [undefined, 791725056], + contexts: getAllocatorDumps(processMemoryDumps, 'v8/<unspecified>'), + }); + + const suballocationsSubRow = rootRow.subRows[3]; + checkRow(columns, suballocationsSubRow, { + title(formattedTitle) { + assert.strictEqual( + Polymer.dom(formattedTitle).textContent, 'suballocations'); + assert.strictEqual(formattedTitle.title, ''); + }, + size: [135266304, 272629760], + effective_size: [273678336, 279969792], + sub_row_count: 3, + contexts: [SUBALLOCATION_CONTEXT, SUBALLOCATION_CONTEXT], + }); + + const oilpanSuballocationSubRow = suballocationsSubRow.subRows[0]; + checkRow(columns, oilpanSuballocationSubRow, { + title(formattedTitle) { + assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'oilpan'); + assert.strictEqual(formattedTitle.title, ''); + }, + size: [125829120, 268435456], + effective_size: [251658240, 268435456], + sub_row_count: 2, + contexts: [SUBALLOCATION_CONTEXT, SUBALLOCATION_CONTEXT], + }); + + const oilpanUnspecifiedSuballocationSubRow = + oilpanSuballocationSubRow.subRows[0]; + checkRow(columns, oilpanUnspecifiedSuballocationSubRow, { + title(formattedTitle) { + assert.strictEqual( + Polymer.dom(formattedTitle).textContent, '<unspecified>'); + assert.strictEqual(formattedTitle.title, 'v8/__99BEAD'); + }, + size: [75497472, 268435456], + effective_size: [150994944, 268435456], + contexts: getAllocatorDumps(processMemoryDumps, 'v8/__99BEAD'), + }); + + const oilpanAnimalsSuballocationSubRow = + oilpanSuballocationSubRow.subRows[1]; + checkRow(columns, oilpanAnimalsSuballocationSubRow, { + title(formattedTitle) { + assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'animals'); + assert.strictEqual(formattedTitle.title, ''); + }, + size: [50331648, undefined], + effective_size: [100663296, undefined], + sub_row_count: 2, + contexts: [SUBALLOCATION_CONTEXT, undefined], + }); + + const oilpanCowSuballocationSubRow = + oilpanAnimalsSuballocationSubRow.subRows[0]; + checkRow(columns, oilpanCowSuballocationSubRow, { + title(formattedTitle) { + assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'cow'); + assert.strictEqual(formattedTitle.title, 'v8/__42BEEF'); + }, + size: [33554432, undefined], + effective_size: [67108864, undefined], + contexts: getAllocatorDumps(processMemoryDumps, 'v8/__42BEEF'), + }); + + const skiaSuballocationSubRow = suballocationsSubRow.subRows[1]; + checkRow(columns, skiaSuballocationSubRow, { + title(formattedTitle) { + assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'skia'); + assert.strictEqual(formattedTitle.title, 'v8/__15FADE'); + }, + size: [8388608, undefined], + effective_size: [16777216, undefined], + contexts: getAllocatorDumps(processMemoryDumps, 'v8/__15FADE'), + }); + + const ccSuballocationSubRow = suballocationsSubRow.subRows[2]; + checkRow(columns, ccSuballocationSubRow, { + title(formattedTitle) { + assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'cc'); + assert.strictEqual(formattedTitle.title, 'v8/__12FEED, v8/__13DEED'); + }, + size: [1048576, 4194304], + effective_size: [5242880, 11534336], + url: ['localhost:1234', 'localhost:5678'], + contexts: [ + processMemoryDumps[0].getMemoryAllocatorDumpByFullName('v8/__12FEED'), + processMemoryDumps[1].getMemoryAllocatorDumpByFullName('v8/__13DEED') + ] + }); + }); + + test('instantiate_multipleMax', function() { + const processMemoryDumps = createProcessMemoryDumps(); + + const viewEl = tr.ui.analysis.createTestPane( + 'tr-ui-a-memory-dump-allocator-details-pane'); + viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8'); + viewEl.aggregationMode = AggregationMode.MAX; + viewEl.rebuild(); + assert.deepEqual(viewEl.requestedChildPanes, [undefined]); + this.addHTMLOutput(viewEl); + + // Check that the table is shown. + assert.isTrue(isElementDisplayed(viewEl.$.table)); + assert.isFalse(isElementDisplayed(viewEl.$.info_text)); + + // Just check that the aggregation mode was propagated to the columns. + const table = viewEl.$.table; + const columns = table.tableColumns; + checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.MAX); + const rows = table.tableRows; + assert.lengthOf(rows, 1); + }); + + test('instantiate_multipleWithUndefined', function() { + const processMemoryDumps = createProcessMemoryDumps(); + processMemoryDumps.splice(1, 0, undefined); + + const viewEl = tr.ui.analysis.createTestPane( + 'tr-ui-a-memory-dump-allocator-details-pane'); + viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8'); + viewEl.aggregationMode = AggregationMode.DIFF; + viewEl.rebuild(); + assert.deepEqual(viewEl.requestedChildPanes, [undefined]); + this.addHTMLOutput(viewEl); + + // Check that the table is shown. + assert.isTrue(isElementDisplayed(viewEl.$.table)); + assert.isFalse(isElementDisplayed(viewEl.$.info_text)); + + const table = viewEl.$.table; + const columns = table.tableColumns; + checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.DIFF); + const rows = table.tableRows; + assert.lengthOf(rows, 1); + + // Check only a few rows of the table. + const rootRow = rows[0]; + checkRow(columns, rootRow, { + title: 'v8', + size: [942619648, undefined, 1066401792], + effective_size: [1081031680, undefined, 1073741824], + inner_size: [2097152, undefined, 2097152], + objects_count: [204, undefined, 204], + sub_row_count: 4, + contexts: getAllocatorDumps(processMemoryDumps, 'v8'), + }); + + const unspecifiedSubRow = rootRow.subRows[2]; + checkRow(columns, unspecifiedSubRow, { + title: '<unspecified>', + size: [undefined, undefined, 791725056], + effective_size: [undefined, undefined, 791725056], + contexts: getAllocatorDumps(processMemoryDumps, 'v8/<unspecified>'), + }); + + const suballocationsSubRow = rootRow.subRows[3]; + checkRow(columns, suballocationsSubRow, { + title(formattedTitle) { + assert.strictEqual( + Polymer.dom(formattedTitle).textContent, 'suballocations'); + assert.strictEqual(formattedTitle.title, ''); + }, + size: [135266304, undefined, 272629760], + effective_size: [273678336, undefined, 279969792], + sub_row_count: 3, + contexts: [SUBALLOCATION_CONTEXT, undefined, SUBALLOCATION_CONTEXT], + }); + }); + + test('heapDumpsPassThrough', function() { + const processMemoryDumps = createProcessMemoryDumps(); + const heapDumps = processMemoryDumps.map(function(dump) { + if (dump === undefined) return undefined; + return new HeapDump(dump, 'v8'); + }); + + // Start by creating a component details pane without any heap dumps. + const viewEl = tr.ui.analysis.createTestPane( + 'tr-ui-a-memory-dump-allocator-details-pane'); + viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8'); + viewEl.aggregationMode = AggregationMode.MAX; + viewEl.rebuild(); + + assert.lengthOf(viewEl.requestedChildPanes, 1); + assert.isUndefined(viewEl.requestedChildPanes[0]); + + // Set the heap dumps. This should trigger creating a heap details pane. + viewEl.heapDumps = heapDumps; + viewEl.aggregationMode = AggregationMode.DIFF; + viewEl.rebuild(); + + assert.lengthOf(viewEl.requestedChildPanes, 2); + assert.strictEqual(viewEl.requestedChildPanes[1].tagName, + 'TR-UI-A-MEMORY-DUMP-HEAP-DETAILS-PANE'); + assert.strictEqual(viewEl.requestedChildPanes[1].heapDumps, heapDumps); + assert.strictEqual(viewEl.requestedChildPanes[1].aggregationMode, + AggregationMode.DIFF); + + // Unset the heap dumps. This should trigger removing the heap details pane. + viewEl.heapDumps = undefined; + viewEl.rebuild(); + + assert.lengthOf(viewEl.requestedChildPanes, 3); + assert.isUndefined(viewEl.requestedChildPanes[2]); + }); + + test('allocatorDumpNameColumn', function() { + const c = new AllocatorDumpNameColumn(); + + // Regular row. + assert.strictEqual(c.formatTitle({title: 'Regular row'}), 'Regular row'); + + // Sub-allocation row. + const row = c.formatTitle({ + title: 'Suballocation row', + suballocation: true, + }); + assert.strictEqual(Polymer.dom(row).textContent, 'Suballocation row'); + assert.strictEqual(row.style.fontStyle, 'italic'); + }); + + test('effectiveSizeColumn_noContext', function() { + const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x), + AggregationMode.DIFF); + + // Single selection. + checkColumnInfosAndColor(c, + createSizeFields([128]), + undefined /* no context */, + [] /* no infos */, + undefined /* no color */); + + // Multi-selection. + checkColumnInfosAndColor(c, + createSizeFields([128, 256, undefined, 64]), + undefined /* no context */, + [] /* no infos */, + undefined /* no color */); + }); + + test('effectiveSizeColumn_suballocationContext', function() { + const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x), + AggregationMode.MAX); + + // Single selection. + checkColumnInfosAndColor(c, + createSizeFields([128]), + [SUBALLOCATION_CONTEXT], + [] /* no infos */, + undefined /* no color */); + + // Multi-selection. + checkColumnInfosAndColor(c, + createSizeFields([undefined, 256, undefined, 64]), + [undefined, SUBALLOCATION_CONTEXT, SUBALLOCATION_CONTEXT, + SUBALLOCATION_CONTEXT], + [] /* no infos */, + undefined /* no color */); + }); + + test('effectiveSizeColumn_dumpContext_noOwnership', function() { + const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x), + AggregationMode.DIFF); + const pmds = buildProcessMemoryDumps(4 /* count */, function(pmds) { + addRootDumps(pmds[0], ['v8'], function(v8Dump) { + addChildDump(v8Dump, 'heaps', {numerics: {size: 64}}); + }); + addRootDumps(pmds[2], ['v8'], function(v8Dump) { + addChildDump(v8Dump, 'heaps', {numerics: {size: 128}}); + }); + addRootDumps(pmds[3], ['v8'], function(v8Dump) {}); + }); + const v8HeapsDumps = getAllocatorDumps(pmds, 'v8/heaps'); + + // Single selection. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapsDumps[0]], + 'effective_size', + [] /* no infos */); + + // Multi-selection, all dumps defined. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapsDumps[0], v8HeapsDumps[2]], + 'effective_size', + [] /* no infos */); + + // Multi-selection, some dumps missing. + checkAllocatorPaneColumnInfosAndColor(c, + v8HeapsDumps, + 'effective_size', + [] /* no infos */); + }); + + test('effectiveSizeColumn_dumpContext_singleOwnership', function() { + const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x), + AggregationMode.MAX); + const pmds = buildProcessMemoryDumps(5 /* count */, function(pmds) { + addRootDumps(pmds[0], ['v8', 'oilpan'], function(v8Dump, oilpanDump) { + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 32}}); + const oilpanObjectsDump = + addChildDump(oilpanDump, 'objects', {numerics: {size: 64}}); + addOwnershipLink(v8HeapsDump, oilpanObjectsDump); + }); + addRootDumps(pmds[1], ['v8'], function(v8Dump) { + addChildDump(v8Dump, 'heaps', {numerics: {size: 32}}); + // Missing oilpan/objects dump. + }); + addRootDumps(pmds[2], ['v8', 'oilpan'], function(v8Dump, oilpanDump) { + addChildDump(oilpanDump, 'objects', {numerics: {size: 64}}); + // Missing v8/heaps dump. + }); + addRootDumps(pmds[3], ['v8', 'oilpan'], function(v8Dump, oilpanDump) { + addChildDump(v8Dump, 'heaps', {numerics: {size: 32}}); + addChildDump(oilpanDump, 'objects', {numerics: {size: 64}}); + // Missing ownership link. + }); + addRootDumps(pmds[4], ['v8', 'oilpan'], function(v8Dump, oilpanDump) { + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 32}}); + const oilpanObjectsDump = + addChildDump(oilpanDump, 'objects', {numerics: {size: 64}}); + addOwnershipLink(v8HeapsDump, oilpanObjectsDump, 2); + }); + }); + const v8HeapsDump = getAllocatorDumps(pmds, 'v8/heaps'); + const oilpanObjectsDump = getAllocatorDumps(pmds, 'oilpan/objects'); + + // Single selection. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapsDump[0]], + 'effective_size', + [ + { + icon: '\u21FE', + message: 'shares \'oilpan/objects\' in Process 1 (importance: 0) ' + + 'with no other dumps', + color: 'green' + } + ]); + checkAllocatorPaneColumnInfosAndColor(c, + [oilpanObjectsDump[4]], + 'effective_size', + [ + { + icon: '\u21FD', + message: 'shared by \'v8/heaps\' in Process 1 (importance: 2)', + color: 'green' + } + ]); + + // Multi-selection, all dumps defined. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapsDump[0], v8HeapsDump[4]], + 'effective_size', + [ + { + icon: '\u21FE', + message: 'shares \'oilpan/objects\' in Process 1 (importance: ' + + '0\u20132) with no other dumps', + color: 'green' + } + ]); + checkAllocatorPaneColumnInfosAndColor(c, + [oilpanObjectsDump[0], oilpanObjectsDump[4]], + 'effective_size', + [ + { + icon: '\u21FD', + message: 'shared by \'v8/heaps\' in Process 1 (importance: ' + + '0\u20132)', + color: 'green' + } + ]); + + // Multi-selection, some dumps missing. + checkAllocatorPaneColumnInfosAndColor(c, + v8HeapsDump, + 'effective_size', + [ + { + icon: '\u21FE', + message: 'shares \'oilpan/objects\' in Process 1 at some ' + + 'selected timestamps (importance: 0\u20132) with no other ' + + 'dumps', + color: 'green' + } + ]); + checkAllocatorPaneColumnInfosAndColor(c, + oilpanObjectsDump, + 'effective_size', + [ + { + icon: '\u21FD', + message: 'shared by \'v8/heaps\' in Process 1 at some selected ' + + 'timestamps (importance: 0\u20132)', + color: 'green' + } + ]); + }); + + test('effectiveSizeColumn_dumpContext_multipleOwnerships', function() { + const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x), + AggregationMode.DIFF); + const pmds = buildProcessMemoryDumps(6 /* count */, function(pmds) { + addRootDumps(pmds[0], ['v8', 'oilpan'], function(v8Dump, oilpanDump) { + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 32}}); + const v8QueuesDump = addChildDump(v8Dump, 'queues', + {numerics: {size: 8}}); + const oilpanObjectsDump = + addChildDump(oilpanDump, 'objects', {numerics: {size: 64}}); + addOwnershipLink(v8HeapsDump, oilpanObjectsDump); + addOwnershipLink(v8QueuesDump, oilpanObjectsDump, 1); + }); + addRootDumps(pmds[1], ['v8'], function(v8Dump) {}); + addRootDumps(pmds[2], ['v8', 'oilpan'], function(v8Dump, oilpanDump) { + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 32}}); + const v8QueuesDump = addChildDump(v8Dump, 'queues', + {numerics: {size: 8}}); + const v8PilesDump = addChildDump(v8Dump, 'piles', + {numerics: {size: 48}}); + const oilpanObjectsDump = + addChildDump(oilpanDump, 'objects', {numerics: {size: 64}}); + addOwnershipLink(v8HeapsDump, oilpanObjectsDump, 2); + addOwnershipLink(v8QueuesDump, oilpanObjectsDump, 1); + addOwnershipLink(v8PilesDump, oilpanObjectsDump); + }); + addRootDumps(pmds[3], ['v8', 'blink'], function(v8Dump, blinkDump) { + const blinkHandlesDump = addChildDump(blinkDump, 'handles', + {numerics: {size: 32}}); + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 64}}); + const blinkObjectsDump = addChildDump(blinkDump, 'objects', + {numerics: {size: 32}}); + addOwnershipLink(blinkHandlesDump, v8HeapsDump, -273); + addOwnershipLink(v8HeapsDump, blinkObjectsDump, 3); + }); + addRootDumps(pmds[4], ['v8', 'gpu'], function(v8Dump, gpuDump) { + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 64}}); + const gpuTile1Dump = addChildDump(gpuDump, 'tile1', + {numerics: {size: 100}}); + const gpuTile2Dump = addChildDump(gpuDump, 'tile2', + {numerics: {size: 99}}); + addOwnershipLink(v8HeapsDump, gpuTile1Dump, 3); + addOwnershipLink(gpuTile2Dump, gpuTile1Dump, -1); + }); + addRootDumps(pmds[5], ['v8', 'oilpan'], function(v8Dump, oilpanDump) { + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 32}}); + const v8QueuesDump = addChildDump(v8Dump, 'queues', + {numerics: {size: 8}}); + const v8PilesDump = addChildDump(v8Dump, 'piles', + {numerics: {size: 48}}); + const oilpanObjectsDump = + addChildDump(oilpanDump, 'objects', {numerics: {size: 64}}); + addOwnershipLink(v8HeapsDump, oilpanObjectsDump, 1); + addOwnershipLink(v8QueuesDump, oilpanObjectsDump, 1); + addOwnershipLink(v8PilesDump, oilpanObjectsDump, 7); + }); + }); + const v8HeapsDump = getAllocatorDumps(pmds, 'v8/heaps'); + const oilpanObjectsDump = getAllocatorDumps(pmds, 'oilpan/objects'); + const gpuTile1Dump = getAllocatorDumps(pmds, 'gpu/tile1'); + + // Single selection. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapsDump[4]], + 'effective_size', + [ + { + icon: '\u21FE', + message: 'shares \'gpu/tile1\' in Process 1 (importance: 3) with ' + + '\'gpu/tile2\' in Process 1 (importance: -1)', + color: 'green' + } + ]); + checkAllocatorPaneColumnInfosAndColor(c, + [gpuTile1Dump[4]], + 'effective_size', + [ + { + icon: '\u21FD', + message: 'shared by:\n' + + ' - \'v8/heaps\' in Process 1 (importance: 3)\n' + + ' - \'gpu/tile2\' in Process 1 (importance: -1)', + color: 'green' + } + ]); + + // Multi-selection, all dumps defined. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapsDump[2], v8HeapsDump[5]], + 'effective_size', + [ + { + icon: '\u21FE', + message: 'shares \'oilpan/objects\' in Process 1 (importance: ' + + '1\u20132) with:\n' + + ' - \'v8/queues\' in Process 1 (importance: 1)\n' + + ' - \'v8/piles\' in Process 1 (importance: 0\u20137)', + color: 'green' + } + ]); + checkAllocatorPaneColumnInfosAndColor(c, + [oilpanObjectsDump[2], oilpanObjectsDump[5]], + 'effective_size', + [ + { + icon: '\u21FD', + message: 'shared by:\n' + + ' - \'v8/heaps\' in Process 1 (importance: 1\u20132)\n' + + ' - \'v8/queues\' in Process 1 (importance: 1)\n' + + ' - \'v8/piles\' in Process 1 (importance: 0\u20137)', + color: 'green' + } + ]); + + // Multi-selection, some dumps missing. + checkAllocatorPaneColumnInfosAndColor(c, + v8HeapsDump, + 'effective_size', + [ // v8/objects is both owned (first info) and an owner (second info). + { + icon: '\u21FD', + message: 'shared by \'blink/handles\' in Process 1 at some ' + + 'selected timestamps (importance: -273)', + color: 'green' + }, + { + icon: '\u21FE', + message: 'shares:\n' + + ' - \'oilpan/objects\' in Process 1 at some selected ' + + 'timestamps (importance: 0\u20132) with:\n' + + ' - \'v8/queues\' in Process 1 (importance: 1)\n' + + ' - \'v8/piles\' in Process 1 at some selected ' + + 'timestamps (importance: 0\u20137)\n' + + ' - \'blink/objects\' in Process 1 at some selected ' + + 'timestamps (importance: 3) with no other dumps\n' + + ' - \'gpu/tile1\' in Process 1 at some selected timestamps ' + + '(importance: 3) with \'gpu/tile2\' in Process 1 ' + + '(importance: -1)', + color: 'green' + } + ]); + checkAllocatorPaneColumnInfosAndColor(c, + oilpanObjectsDump, + 'effective_size', + [ + { + icon: '\u21FD', + message: 'shared by:\n' + + ' - \'v8/heaps\' in Process 1 at some selected timestamps ' + + '(importance: 0\u20132)\n' + + ' - \'v8/queues\' in Process 1 at some selected timestamps ' + + '(importance: 1)\n' + + ' - \'v8/piles\' in Process 1 at some selected timestamps ' + + '(importance: 0\u20137)', + color: 'green' + } + ]); + }); + + test('sizeColumn_noContext', function() { + const c = new SizeColumn('Size', 'bytes', (x => x), + AggregationMode.DIFF); + + // Single selection. + checkColumnInfosAndColor(c, + createSizeFields([128]), + undefined /* no context */, + [] /* no infos */, + undefined /* no color */); + + // Multi-selection. + checkColumnInfosAndColor(c, + createSizeFields([128, 256, undefined, 64]), + undefined /* no context */, + [] /* no infos */, + undefined /* no color */); + }); + + test('sizeColumn_suballocationContext', function() { + const c = new SizeColumn('Size', 'bytes', (x => x), + AggregationMode.MAX); + + // Single selection. + checkColumnInfosAndColor(c, + createSizeFields([128]), + [SUBALLOCATION_CONTEXT], + [] /* no infos */, + undefined /* no color */); + + // Multi-selection. + checkColumnInfosAndColor(c, + createSizeFields([undefined, 256, undefined, 64]), + [undefined, SUBALLOCATION_CONTEXT, undefined, SUBALLOCATION_CONTEXT], + [] /* no infos */, + undefined /* no color */); + }); + + test('sizeColumn_dumpContext', function() { + const c = new SizeColumn('Size', 'bytes', (x => x), AggregationMode.DIFF); + const pmds = buildProcessMemoryDumps(7 /* count */, function(pmds) { + addRootDumps(pmds[0], ['v8'], function(v8Dump) { + // Single direct overlap (v8/objects -> v8/heaps). + const v8ObjectsDump = addChildDump(v8Dump, 'objects', + {numerics: {size: 1536}}); + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 2048}}); + addOwnershipLink(v8ObjectsDump, v8HeapsDump); + }); + // pmd[1] intentionally skipped. + addRootDumps(pmds[2], ['v8'], function(v8Dump, oilpanDump) { + // Single direct overlap with inconsistent owned dump size. + const v8ObjectsDump = addChildDump(v8Dump, 'objects', + {numerics: {size: 3072}}); + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 2048}}); + addOwnershipLink(v8ObjectsDump, v8HeapsDump); + }); + addRootDumps(pmds[3], ['v8'], function(v8Dump) { + // Single indirect overlap (v8/objects/X -> v8/heaps/42). + const v8ObjectsDump = addChildDump(v8Dump, 'objects', + {numerics: {size: 1536}}); + const v8ObjectsXDump = addChildDump(v8ObjectsDump, 'X', + {numerics: {size: 512}}); + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 2048}}); + const v8Heaps42Dump = addChildDump(v8HeapsDump, '42', + {numerics: {size: 1024}}); + addOwnershipLink(v8ObjectsXDump, v8Heaps42Dump); + }); + addRootDumps(pmds[4], ['v8'], function(v8Dump) { + // Multiple overlaps. + const v8ObjectsDump = addChildDump(v8Dump, 'objects', + {numerics: {size: 1024}}); + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 2048}}); + + const v8ObjectsXDump = addChildDump(v8ObjectsDump, 'X', + {numerics: {size: 512}}); + const v8Heaps42Dump = addChildDump(v8HeapsDump, '42', + {numerics: {size: 1280}}); + addOwnershipLink(v8ObjectsXDump, v8Heaps42Dump); + + const v8ObjectsYDump = addChildDump(v8ObjectsDump, 'Y', + {numerics: {size: 128}}); + const v8Heaps90Dump = addChildDump(v8HeapsDump, '90', + {numerics: {size: 256}}); + addOwnershipLink(v8ObjectsYDump, v8Heaps90Dump); + + const v8BlocksDump = addChildDump(v8Dump, 'blocks', + {numerics: {size: 768}}); + addOwnershipLink(v8BlocksDump, v8Heaps42Dump); + }); + addRootDumps(pmds[5], ['v8'], function(v8Dump) { + // No overlaps, inconsistent parent size. + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 2048}}); + addChildDump(v8HeapsDump, '42', {numerics: {size: 1536}}); + addChildDump(v8HeapsDump, '90', {numerics: {size: 615}}); + }); + addRootDumps(pmds[6], ['v8', 'oilpan'], function(v8Dump, oilpanDump) { + // No overlaps, inconsistent parent and owned dump size. + const v8HeapsDump = addChildDump(v8Dump, 'heaps', + {numerics: {size: 2048}}); + addChildDump(v8HeapsDump, '42', {numerics: {size: 1536}}); + addChildDump(v8HeapsDump, '90', {numerics: {size: 615}}); + const oilpanObjectsDump = + addChildDump(oilpanDump, 'objects', {numerics: {size: 3072}}); + addOwnershipLink(oilpanObjectsDump, v8HeapsDump); + }); + }); + const v8HeapDumps = getAllocatorDumps(pmds, 'v8/heaps'); + + // Single selection, single overlap. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapDumps[0]], + 'size', + [ + { + icon: '\u24D8', + message: 'overlaps with its sibling \'objects\' (1.5 KiB)', + color: 'blue' + } + ]); + + // Single selection, multiple overlaps. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapDumps[4]], + 'size', + [ + { + icon: '\u24D8', + message: 'overlaps with its siblings:\n' + + ' - \'objects\' (640.0 B)\n' + + ' - \'blocks\' (768.0 B)', + color: 'blue' + } + ]); + + // Single selection, warnings with no overlaps. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapDumps[6]], + 'size', + [ + { + icon: '\u26A0', + message: 'provided size (2.0 KiB) was less than the aggregated ' + + 'size of the children (2.1 KiB)', + color: 'red' + }, + { + icon: '\u26A0', + message: 'provided size (2.0 KiB) was less than the size of the ' + + 'largest owner (3.0 KiB)', + color: 'red' + } + ]); + + // Single selection, single overlap with a warning. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapDumps[2]], + 'size', + [ + { + icon: '\u24D8', + message: 'overlaps with its sibling \'objects\' (3.0 KiB)', + color: 'blue' + }, + { + icon: '\u26A0', + message: 'provided size (2.0 KiB) was less than the size of the ' + + 'largest owner (3.0 KiB)', + color: 'red' + } + ]); + + // Multi-selection, single overlap. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapDumps[0], v8HeapDumps[3]], + 'size', + [ + { + icon: '\u24D8', + message: 'overlaps with its sibling \'objects\'', + color: 'blue' + } + ]); + + // Multi-selection, multiple overlaps. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapDumps[0], v8HeapDumps[4]], + 'size', + [ + { + icon: '\u24D8', + message: 'overlaps with its siblings:\n' + + ' - \'objects\'\n' + + ' - \'blocks\' at some selected timestamps', + color: 'blue' + } + ]); + + // Multi-selection, warnings with no overlaps. + checkAllocatorPaneColumnInfosAndColor(c, + [v8HeapDumps[5], v8HeapDumps[6]], + 'size', + [ + { + icon: '\u26A0', + message: 'provided size was less than the aggregated ' + + 'size of the children', + color: 'red' + }, + { + icon: '\u26A0', + message: 'provided size was less than the size of the largest ' + + 'owner at some selected timestamps', + color: 'red' + } + ]); + + // Multi-selection, multiple overlaps with warnings. + checkAllocatorPaneColumnInfosAndColor(c, + v8HeapDumps, + 'size', + [ + { + icon: '\u24D8', + message: 'overlaps with its siblings:\n' + + ' - \'objects\' at some selected timestamps\n' + + ' - \'blocks\' at some selected timestamps', + color: 'blue' + }, + { + icon: '\u26A0', + message: 'provided size was less than the size of the largest ' + + 'owner at some selected timestamps', + color: 'red' + }, + { + icon: '\u26A0', + message: 'provided size was less than the aggregated size of ' + + 'the children at some selected timestamps', + color: 'red' + } + ]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane.html new file mode 100644 index 00000000000..1141116ec86 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane.html @@ -0,0 +1,178 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_overview_pane.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> +<link rel="import" href="/tracing/ui/analysis/stacked_pane.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> + +<dom-module id='tr-ui-a-memory-dump-header-pane'> + <template> + <style> + :host { + display: flex; + flex-direction: row; + align-items: center; + + background-color: #d0d0d0; + border-bottom: 1px solid #8e8e8e; + border-top: 1px solid white; + } + + #label { + flex: 1 1 auto; + padding: 6px; + font-size: 15px; + } + + #aggregation_mode_container { + display: none; + flex: 0 0 auto; + padding: 5px; + font-size: 15px; + } + </style> + </tr-ui-b-view-specific-brushing-state> + <div id="label"></div> + <div id="aggregation_mode_container"> + <span>Metric aggregation:</span> + <!-- Aggregation mode selector (added in Polymer.ready()) --> + </div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + Polymer({ + is: 'tr-ui-a-memory-dump-header-pane', + behaviors: [tr.ui.analysis.StackedPane], + + created() { + this.containerMemoryDumps_ = undefined; + }, + + ready() { + Polymer.dom(this.$.aggregation_mode_container).appendChild( + tr.ui.b.createSelector(this, 'aggregationMode', + 'memoryDumpHeaderPane.aggregationMode', + tr.ui.analysis.MemoryColumn.AggregationMode.DIFF, [ + { + label: 'Diff', + value: tr.ui.analysis.MemoryColumn.AggregationMode.DIFF + }, + { + label: 'Max', + value: tr.ui.analysis.MemoryColumn.AggregationMode.MAX + } + ])); + }, + + /** + * Sets the container memory dumps and schedules rebuilding the pane. + * + * The provided value should be a chronologically sorted list of + * ContainerMemoryDump objects. All of the dumps must be associated with + * the same container (i.e. containerMemoryDumps must be either a list of + * ProcessMemoryDump(s) belonging to the same process, or a list of + * GlobalMemoryDump(s)). Example: + * + * [ + * tr.model.ProcessMemoryDump {}, // PMD at timestamp 1. + * tr.model.ProcessMemoryDump {}, // PMD at timestamp 2. + * tr.model.ProcessMemoryDump {} // PMD at timestamp 3. + * ] + */ + set containerMemoryDumps(containerMemoryDumps) { + this.containerMemoryDumps_ = containerMemoryDumps; + this.scheduleRebuild_(); + }, + + get containerMemoryDumps() { + return this.containerMemoryDumps_; + }, + + set aggregationMode(aggregationMode) { + this.aggregationMode_ = aggregationMode; + this.scheduleRebuild_(); + }, + + get aggregationMode() { + return this.aggregationMode_; + }, + + onRebuild_() { + this.updateLabel_(); + this.updateAggregationModeSelector_(); + this.changeChildPane_(); + }, + + updateLabel_() { + Polymer.dom(this.$.label).textContent = ''; + + if (this.containerMemoryDumps_ === undefined || + this.containerMemoryDumps_.length <= 0) { + Polymer.dom(this.$.label).textContent = 'No memory dumps selected'; + return; + } + + const containerDumpCount = this.containerMemoryDumps_.length; + const isMultiSelection = containerDumpCount > 1; + + Polymer.dom(this.$.label).appendChild(document.createTextNode( + 'Selected ' + containerDumpCount + ' memory dump' + + (isMultiSelection ? 's' : '') + + ' in ' + this.containerMemoryDumps_[0].containerName + ' at ')); + // TODO(petrcermak): Use <tr-v-ui-scalar-span> once it can be displayed + // inline. See https://github.com/catapult-project/catapult/issues/1371. + Polymer.dom(this.$.label).appendChild(document.createTextNode( + tr.b.Unit.byName.timeStampInMs.format( + this.containerMemoryDumps_[0].start))); + if (isMultiSelection) { + const ELLIPSIS = String.fromCharCode(8230); + Polymer.dom(this.$.label).appendChild( + document.createTextNode(ELLIPSIS)); + Polymer.dom(this.$.label).appendChild(document.createTextNode( + tr.b.Unit.byName.timeStampInMs.format( + this.containerMemoryDumps_[containerDumpCount - 1].start))); + } + }, + + updateAggregationModeSelector_() { + let displayStyle; + if (this.containerMemoryDumps_ === undefined || + this.containerMemoryDumps_.length <= 1) { + displayStyle = 'none'; + } else { + displayStyle = 'initial'; + } + this.$.aggregation_mode_container.style.display = displayStyle; + }, + + changeChildPane_() { + this.childPaneBuilder = function() { + if (this.containerMemoryDumps_ === undefined || + this.containerMemoryDumps_.length <= 0) { + return undefined; + } + + const overviewPane = document.createElement( + 'tr-ui-a-memory-dump-overview-pane'); + overviewPane.processMemoryDumps = this.containerMemoryDumps_.map( + function(containerDump) { + return containerDump.processMemoryDumps; + }); + overviewPane.aggregationMode = this.aggregationMode; + return overviewPane; + }.bind(this); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane_test.html new file mode 100644 index 00000000000..3d5d20a7c47 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane_test.html @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/memory_dump_header_pane.html"> +<link rel="import" + href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode; + const isElementDisplayed = tr.ui.analysis.isElementDisplayed; + + function createAndCheckMemoryDumpHeaderPane(test, containerMemoryDumps, + expectedLabelText, expectedChildPaneRequested, expectedSelectorVisible) { + const viewEl = + tr.ui.analysis.createTestPane('tr-ui-a-memory-dump-header-pane'); + viewEl.containerMemoryDumps = containerMemoryDumps; + viewEl.rebuild(); + test.addHTMLOutput(viewEl); + checkMemoryDumpHeaderPane(viewEl, containerMemoryDumps, expectedLabelText, + expectedChildPaneRequested, expectedSelectorVisible); + } + + function checkMemoryDumpHeaderPane(viewEl, containerMemoryDumps, + expectedLabelText, expectedChildPaneRequested, expectedSelectorVisible) { + // The default aggregation mode is DIFF. + assert.strictEqual(viewEl.aggregationMode, AggregationMode.DIFF); + + // Check the text in the label. + assert.strictEqual( + Polymer.dom(viewEl.$.label).textContent, expectedLabelText); + + // Check the visibility of aggregation mode selector. + const aggregationModeContainerVisible = + isElementDisplayed(viewEl.$.aggregation_mode_container); + const childPanes = viewEl.requestedChildPanes; + + // Check the requested child panes. + if (containerMemoryDumps === undefined || + containerMemoryDumps.length === 0) { + assert.isTrue(!expectedSelectorVisible); // Test sanity check. + assert.isFalse(aggregationModeContainerVisible); + assert.lengthOf(childPanes, 1); + assert.isUndefined(childPanes[0]); + return; + } + + const expectedProcessMemoryDumps = containerMemoryDumps.map( + function(containerMemoryDump) { + return containerMemoryDump.processMemoryDumps; + }); + function checkLastChildPane(expectedChildPaneCount) { + assert.lengthOf(childPanes, expectedChildPaneCount); + const lastChildPane = childPanes[expectedChildPaneCount - 1]; + assert.strictEqual( + lastChildPane.tagName, 'TR-UI-A-MEMORY-DUMP-OVERVIEW-PANE'); + assert.deepEqual(lastChildPane.processMemoryDumps, + expectedProcessMemoryDumps); + assert.strictEqual(lastChildPane.aggregationMode, viewEl.aggregationMode); + } + + checkLastChildPane(1); + + // Check the behavior of aggregation mode selector (if visible). + if (!expectedSelectorVisible) { + assert.isFalse(aggregationModeContainerVisible); + return; + } + + assert.isTrue(aggregationModeContainerVisible); + const selector = tr.ui.b.findDeepElementMatching(viewEl, 'select'); + + selector.selectedValue = AggregationMode.MAX; + viewEl.rebuild(); + assert.strictEqual(viewEl.aggregationMode, AggregationMode.MAX); + checkLastChildPane(2); + + selector.selectedValue = AggregationMode.DIFF; + viewEl.rebuild(); + assert.strictEqual(viewEl.aggregationMode, AggregationMode.DIFF); + checkLastChildPane(3); + } + + test('instantiate_empty', function() { + tr.ui.analysis.createAndCheckEmptyPanes(this, + 'tr-ui-a-memory-dump-header-pane', 'containerMemoryDumps', + function(viewEl) { + checkMemoryDumpHeaderPane(viewEl, [], 'No memory dumps selected', + false /* no child pane requested */, + false /* aggregation mode selector hidden */); + }); + }); + + test('instantiate_singleGlobalMemoryDump', function() { + createAndCheckMemoryDumpHeaderPane(this, + [tr.ui.analysis.createSingleTestGlobalMemoryDump()], + 'Selected 1 memory dump in global space at 68.000 ms', + true /* child pane requested */, + false /* aggregation mode selector hidden */); + }); + + test('instantiate_multipleGlobalMemoryDumps', function() { + createAndCheckMemoryDumpHeaderPane(this, + tr.ui.analysis.createMultipleTestGlobalMemoryDumps(), + 'Selected 3 memory dumps in global space at 42.000 ms\u2026100.000 ms', + true /* child pane requested */, + true /* aggregation selector visible */); + }); + + test('instantiate_singleProcessMemoryDump', function() { + createAndCheckMemoryDumpHeaderPane(this, + [tr.ui.analysis.createSingleTestProcessMemoryDump()], + 'Selected 1 memory dump in Process 2 at 69.000 ms', + true /* child pane requested */, + false /* aggregation mode selector hidden */); + }); + + test('instantiate_multipleProcessMemoryDumps', function() { + createAndCheckMemoryDumpHeaderPane(this, + tr.ui.analysis.createMultipleTestProcessMemoryDumps(), + 'Selected 3 memory dumps in Process 2 at 42.000 ms\u2026102.000 ms', + true /* child pane requested */, + true /* aggregation selector visible */); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html new file mode 100644 index 00000000000..9d17e39ce85 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html @@ -0,0 +1,354 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/base/event.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_util.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> +<link rel="import" href="/tracing/ui/analysis/rebuildable_behavior.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/tab_view.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/scalar_context_controller.html"> + +<dom-module id='tr-ui-a-memory-dump-heap-details-breakdown-view'> + <template> + <tr-ui-b-tab-view id="tabs"></tr-ui-b-tab-view> + </template> +</dom-module> + +<dom-module id='tr-ui-a-memory-dump-heap-details-breakdown-view-tab'> + <template> + <tr-v-ui-scalar-context-controller></tr-v-ui-scalar-context-controller> + <tr-ui-b-info-bar id="info" hidden></tr-ui-b-info-bar> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> + +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const RESONABLE_NUMBER_OF_ROWS = 200; + + const TabUiState = { + NO_LONG_TAIL: 0, + HIDING_LONG_TAIL: 1, + SHOWING_LONG_TAIL: 2, + }; + + /** @constructor */ + function EmptyFillerColumn() {} + + EmptyFillerColumn.prototype = { + title: '', + + value() { + return ''; + }, + }; + + Polymer({ + is: 'tr-ui-a-memory-dump-heap-details-breakdown-view', + behaviors: [tr.ui.analysis.RebuildableBehavior], + + created() { + this.displayedNode_ = undefined; + this.dimensionToTab_ = new Map(); + }, + + ready() { + this.scheduleRebuild_(); + this.root.addEventListener('keydown', this.onKeyDown_.bind(this), true); + }, + + get displayedNode() { + return this.displayedNode_; + }, + + set displayedNode(node) { + this.displayedNode_ = node; + this.scheduleRebuild_(); + }, + + get aggregationMode() { + return this.aggregationMode_; + }, + + set aggregationMode(aggregationMode) { + this.aggregationMode_ = aggregationMode; + for (const tab of this.$.tabs.tabs) { + tab.aggregationMode = aggregationMode; + } + }, + + onRebuild_() { + const previouslySelectedTab = this.$.tabs.selectedSubView; + let previouslySelectedTabFocused = false; + let previouslySelectedDimension = undefined; + if (previouslySelectedTab) { + previouslySelectedTabFocused = previouslySelectedTab.isFocused; + previouslySelectedDimension = previouslySelectedTab.dimension; + } + + for (const tab of this.$.tabs.tabs) { + tab.nodes = undefined; + } + this.$.tabs.clearSubViews(); + + if (this.displayedNode_ === undefined) { + this.$.tabs.label = 'No heap node provided.'; + return; + } + + for (const [dimension, children] of this.displayedNode_.childNodes) { + if (!this.dimensionToTab_.has(dimension)) { + this.dimensionToTab_.set(dimension, document.createElement( + 'tr-ui-a-memory-dump-heap-details-breakdown-view-tab')); + } + const tab = this.dimensionToTab_.get(dimension); + tab.aggregationMode = this.aggregationMode_; + tab.dimension = dimension; + tab.nodes = children; + this.$.tabs.addSubView(tab); + tab.rebuild(); + if (dimension === previouslySelectedDimension) { + this.$.tabs.selectedSubView = tab; + if (previouslySelectedTabFocused) { + tab.focus(); + } + } + } + + if (this.$.tabs.tabs.length > 0) { + this.$.tabs.label = 'Break selected node further by:'; + } else { + this.$.tabs.label = 'Selected node cannot be broken down any further.'; + } + }, + + onKeyDown_(keyEvent) { + if (!this.displayedNode_) return; + + let keyHandled = false; + switch (keyEvent.keyCode) { + case 8: { + // Backspace. + if (!this.displayedNode_.parentNode) break; + + // Enter the parent node upon pressing backspace. + const viewEvent = new tr.b.Event('enter-node'); + viewEvent.node = this.displayedNode_.parentNode; + this.dispatchEvent(viewEvent); + keyHandled = true; + break; + } + + case 37: // Left arrow. + case 39: // Right arrow. + { + const wasFocused = this.$.tabs.selectedSubView.isFocused; + keyHandled = keyEvent.keyCode === 37 ? + this.$.tabs.selectPreviousTabIfPossible() : + this.$.tabs.selectNextTabIfPossible(); + if (wasFocused && keyHandled) { + this.$.tabs.selectedSubView.focus(); // Restore focus to new tab. + } + } + } + + if (!keyHandled) return; + keyEvent.stopPropagation(); + keyEvent.preventDefault(); + } + }); + + Polymer({ + is: 'tr-ui-a-memory-dump-heap-details-breakdown-view-tab', + behaviors: [tr.ui.analysis.RebuildableBehavior], + + created() { + this.dimension_ = undefined; + this.nodes_ = undefined; + this.aggregationMode_ = undefined; + this.displayLongTail_ = false; + }, + + ready() { + this.$.table.addEventListener('step-into', function(tableEvent) { + const viewEvent = new tr.b.Event('enter-node'); + viewEvent.node = tableEvent.tableRow; + this.dispatchEvent(viewEvent); + }.bind(this)); + }, + + get displayLongTail() { + return this.displayLongTail_; + }, + + set displayLongTail(newValue) { + if (this.displayLongTail === newValue) return; + this.displayLongTail_ = newValue; + this.scheduleRebuild_(); + }, + + get dimension() { + return this.dimension_; + }, + + set dimension(dimension) { + this.dimension_ = dimension; + this.scheduleRebuild_(); + }, + + get nodes() { + return this.nodes_; + }, + + set nodes(nodes) { + this.nodes_ = nodes; + this.scheduleRebuild_(); + }, + + get nodes() { + return this.nodes_ || []; + }, + + get dimensionLabel_() { + if (this.dimension_ === undefined) return '(undefined)'; + return this.dimension_.label; + }, + + get tabLabel() { + let nodeCount = 0; + if (this.nodes_) { + nodeCount = this.nodes_.length; + } + return this.dimensionLabel_ + ' (' + nodeCount + ')'; + }, + + get tabIcon() { + if (this.dimension_ === undefined || + this.dimension_ === tr.ui.analysis.HeapDetailsRowDimension.ROOT) { + return undefined; + } + return { + text: this.dimension_.symbol, + style: 'color: ' + tr.b.ColorScheme.getColorForReservedNameAsString( + this.dimension_.color) + ';' + }; + }, + + get aggregationMode() { + return this.aggregationMode_; + }, + + set aggregationMode(aggregationMode) { + this.aggregationMode_ = aggregationMode; + this.scheduleRebuild_(); + }, + + focus() { + this.$.table.focus(); + }, + + blur() { + this.$.table.blur(); + }, + + get isFocused() { + return this.$.table.isFocused; + }, + + onRebuild_() { + this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + this.$.table.emptyValue = 'Cannot break down by ' + + this.dimensionLabel_.toLowerCase() + ' any further.'; + const [state, rows] = this.getRows_(); + const total = this.nodes.length; + const displayed = rows.length; + const hidden = total - displayed; + this.updateInfoBar_(state, [total, displayed, hidden]); + this.$.table.tableRows = rows; + this.$.table.tableColumns = this.createColumns_(rows); + if (this.$.table.sortColumnIndex === undefined) { + this.$.table.sortColumnIndex = 0; + this.$.table.sortDescending = false; + } + this.$.table.rebuild(); + }, + + createColumns_(rows) { + const titleColumn = new tr.ui.analysis.HeapDetailsTitleColumn( + this.dimensionLabel_); + titleColumn.width = '400px'; + + const numericColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, { + cellKey: 'cells', + aggregationMode: this.aggregationMode_, + rules: tr.ui.analysis.HEAP_DETAILS_COLUMN_RULES, + shouldSetContextGroup: true + }); + if (numericColumns.length === 0) { + numericColumns.push(new EmptyFillerColumn()); + } + tr.ui.analysis.MemoryColumn.spaceEqually(numericColumns); + + const columns = [titleColumn].concat(numericColumns); + return columns; + }, + + getRows_() { + let rows = this.nodes; + if (rows.length <= RESONABLE_NUMBER_OF_ROWS) { + return [TabUiState.NO_LONG_TAIL, rows]; + } else if (this.displayLongTail) { + return [TabUiState.SHOWING_LONG_TAIL, rows]; + } + const absSize = row => Math.max(row.cells.Size.fields[0].value); + rows.sort((a, b) => absSize(b) - absSize(a)); + rows = rows.slice(0, RESONABLE_NUMBER_OF_ROWS); + return [TabUiState.HIDING_LONG_TAIL, rows]; + }, + + updateInfoBar_(state, rowStats) { + if (state === TabUiState.SHOWING_LONG_TAIL) { + this.longTailVisibleInfoBar_(rowStats); + } else if (state === TabUiState.HIDING_LONG_TAIL) { + this.longTailHiddenInfoBar_(rowStats); + } else { + this.hideInfoBar_(); + } + }, + + longTailVisibleInfoBar_(rowStats) { + const [total, visible, hidden] = rowStats; + const couldHide = total - RESONABLE_NUMBER_OF_ROWS; + this.$.info.message = 'Showing ' + total + ' rows. This may be slow.'; + this.$.info.removeAllButtons(); + const buttonText = 'Hide ' + couldHide + ' rows.'; + this.$.info.addButton(buttonText, () => this.displayLongTail = false); + this.$.info.visible = true; + }, + + longTailHiddenInfoBar_(rowStats) { + const [total, visible, hidden] = rowStats; + this.$.info.message = 'Hiding the smallest ' + hidden + ' rows.'; + this.$.info.removeAllButtons(); + this.$.info.addButton('Show all.', () => this.displayLongTail = true); + this.$.info.visible = true; + }, + + hideInfoBar_() { + this.$.info.visible = false; + }, + + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html new file mode 100644 index 00000000000..a43fdaa8189 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html @@ -0,0 +1,451 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/base/multi_dimensional_view.html"> +<link rel="import" href="/tracing/base/scalar.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_path_view.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_util.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> +<link rel="import" href="/tracing/ui/analysis/stacked_pane.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/base/info_bar.html"> + +<dom-module id='tr-ui-a-memory-dump-heap-details-pane'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + + #header { + flex: 0 0 auto; + display: flex; + flex-direction: row; + align-items: center; + + background-color: #eee; + border-bottom: 1px solid #8e8e8e; + border-top: 1px solid white; + } + + #label { + flex: 1 1 auto; + padding: 8px; + font-size: 15px; + font-weight: bold; + } + + #view_mode_container { + display: none; + flex: 0 0 auto; + padding: 5px; + font-size: 15px; + } + + #contents { + flex: 1 0 auto; + align-self: stretch; + font-size: 12px; + } + + #info_text { + padding: 8px; + color: #666; + font-style: italic; + text-align: center; + } + + #split_view { + display: none; /* Hide until memory allocator dumps are set. */ + flex: 1 0 auto; + align-self: stretch; + flex-direction: row; + } + + #path_view { + width: 50%; + } + + #breakdown_view { + flex: 1 1 auto; + width: 0; + } + + #path_view, #breakdown_view { + overflow-x: auto; /* Show scrollbar if necessary. */ + } + </style> + <div id="header"> + <div id="label">Heap details</div> + <div id="view_mode_container"> + <span>View mode:</span> + <!-- View mode selector (added in Polymer.ready()) --> + </div> + </div> + <div id="contents"> + <tr-ui-b-info-bar id="info_bar" hidden> + </tr-ui-b-info-bar> + + <div id="info_text">No heap dump selected</div> + + <div id="split_view"> + <tr-ui-a-memory-dump-heap-details-path-view id="path_view"> + </tr-ui-a-memory-dump-heap-details-path-view> + <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle> + <tr-ui-a-memory-dump-heap-details-breakdown-view id="breakdown_view"> + </tr-ui-a-memory-dump-heap-details-breakdown-view> + </div> + </div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const Scalar = tr.b.Scalar; + const sizeInBytes_smallerIsBetter = + tr.b.Unit.byName.sizeInBytes_smallerIsBetter; + const count_smallerIsBetter = tr.b.Unit.byName.count_smallerIsBetter; + const MultiDimensionalViewBuilder = tr.b.MultiDimensionalViewBuilder; + const TotalState = tr.b.MultiDimensionalViewNode.TotalState; + + /** @{constructor} */ + function HeapDumpTreeNode( + stackFrameNodes, dimension, title, heavyView, parentNode) { + this.dimension = dimension; + this.title = title; + this.parentNode = parentNode; + + this.heavyView_ = heavyView; + this.stackFrameNodes_ = stackFrameNodes; + this.lazyCells_ = undefined; + this.lazyChildNodes_ = undefined; + } + + HeapDumpTreeNode.prototype = { + get minDisplayedTotalState_() { + if (this.heavyView_) { + // Show lower-bound and exact values in heavy views. + return TotalState.LOWER_BOUND; + } + // Show only exact values in tree view. + return TotalState.EXACT; + }, + + get childNodes() { + if (!this.lazyChildNodes_) { + this.lazyChildNodes_ = new Map(); + this.addDimensionChildNodes_( + tr.ui.analysis.HeapDetailsRowDimension.STACK_FRAME, 0); + this.addDimensionChildNodes_( + tr.ui.analysis.HeapDetailsRowDimension.OBJECT_TYPE, 1); + this.releaseStackFrameNodesIfPossible_(); + } + return this.lazyChildNodes_; + }, + + get cells() { + if (!this.lazyCells_) { + this.addCells_(); + this.releaseStackFrameNodesIfPossible_(); + } + return this.lazyCells_; + }, + + releaseStackFrameNodesIfPossible_() { + if (this.lazyCells_ && this.lazyChildNodes_) { + // Don't unnecessarily hold a reference to the stack frame nodes when + // we don't need them anymore. + this.stackFrameNodes_ = undefined; + } + }, + + addDimensionChildNodes_(dimension, dimensionIndex) { + // Child title -> Timestamp (list index) -> Child + // MultiDimensionalViewNode. + const dimensionChildTitleToStackFrameNodes = tr.b.invertArrayOfDicts( + this.stackFrameNodes_, + node => this.convertStackFrameNodeDimensionToChildDict_( + node, dimensionIndex)); + + // Child title (list index) -> Child HeapDumpTreeNode. + const dimensionChildNodes = []; + for (const [childTitle, childStackFrameNodes] of + Object.entries(dimensionChildTitleToStackFrameNodes)) { + dimensionChildNodes.push(new HeapDumpTreeNode(childStackFrameNodes, + dimension, childTitle, this.heavyView_, this)); + } + this.lazyChildNodes_.set(dimension, dimensionChildNodes); + }, + + convertStackFrameNodeDimensionToChildDict_( + stackFrameNode, dimensionIndex) { + const childDict = {}; + let displayedChildrenTotalSize = 0; + let displayedChildrenTotalCount = 0; + let hasDisplayedChildren = false; + let allDisplayedChildrenHaveDisplayedCounts = true; + for (const child of stackFrameNode.children[dimensionIndex].values()) { + if (child.values[0].totalState < this.minDisplayedTotalState_) { + continue; + } + if (child.values[1].totalState < this.minDisplayedTotalState_) { + allDisplayedChildrenHaveDisplayedCounts = false; + } + childDict[child.title[dimensionIndex]] = child; + displayedChildrenTotalSize += child.values[0].total; + displayedChildrenTotalCount += child.values[1].total; + hasDisplayedChildren = true; + } + + const nodeTotalSize = stackFrameNode.values[0].total; + const nodeTotalCount = stackFrameNode.values[1].total; + + // Add '<other>' node if necessary in tree-view. + const hasUnclassifiedSizeOrCount = + displayedChildrenTotalSize < nodeTotalSize || + displayedChildrenTotalCount < nodeTotalCount; + if (!this.heavyView_ && hasUnclassifiedSizeOrCount && + hasDisplayedChildren) { + const otherTitle = stackFrameNode.title.slice(); + otherTitle[dimensionIndex] = '<other>'; + const otherNode = new tr.b.MultiDimensionalViewNode(otherTitle, 2); + childDict[otherTitle[dimensionIndex]] = otherNode; + + // '<other>' node size. + otherNode.values[0].total = nodeTotalSize - displayedChildrenTotalSize; + otherNode.values[0].totalState = this.minDisplayedTotalState_; + + // '<other>' node allocation count. + otherNode.values[1].total = + nodeTotalCount - displayedChildrenTotalCount; + // Don't show allocation count of the '<other>' node if there is a + // displayed child node that did NOT display allocation count. + otherNode.values[1].totalState = + allDisplayedChildrenHaveDisplayedCounts ? + this.minDisplayedTotalState_ : TotalState.NOT_PROVIDED; + } + + return childDict; + }, + + addCells_() { + // Transform a chronological list of heap stack frame tree nodes into a + // dictionary of cells (where each cell contains a chronological list + // of the values of its numeric). + this.lazyCells_ = tr.ui.analysis.createCells(this.stackFrameNodes_, + function(stackFrameNode) { + const size = stackFrameNode.values[0].total; + const numerics = { + 'Size': new Scalar(sizeInBytes_smallerIsBetter, size) + }; + const countValue = stackFrameNode.values[1]; + if (countValue.totalState >= this.minDisplayedTotalState_) { + const count = countValue.total; + numerics.Count = new Scalar( + count_smallerIsBetter, count); + } + return numerics; + }, this); + } + }; + + Polymer({ + is: 'tr-ui-a-memory-dump-heap-details-pane', + behaviors: [tr.ui.analysis.StackedPane], + + created() { + this.heapDumps_ = undefined; + this.viewMode_ = undefined; + this.aggregationMode_ = undefined; + this.cachedBuilders_ = new Map(); + }, + + ready() { + this.$.info_bar.message = 'Note: Values displayed in the heavy view ' + + 'are lower bounds (except for the root).'; + + Polymer.dom(this.$.view_mode_container).appendChild( + tr.ui.b.createSelector( + this, 'viewMode', 'memoryDumpHeapDetailsPane.viewMode', + MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW, + [ + { + label: 'Top-down (Tree)', + value: MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW + }, + { + label: 'Top-down (Heavy)', + value: + MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW + }, + { + label: 'Bottom-up (Heavy)', + value: + MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW + } + ])); + + this.$.drag_handle.target = this.$.path_view; + this.$.drag_handle.horizontal = false; + + // If the user selects a node in the path view, show its children in the + // breakdown view. + this.$.path_view.addEventListener('selected-node-changed', (function(e) { + this.$.breakdown_view.displayedNode = this.$.path_view.selectedNode; + }).bind(this)); + + // If the user double-clicks on a node in the breakdown view, select the + // node in the path view. + this.$.breakdown_view.addEventListener('enter-node', (function(e) { + this.$.path_view.selectedNode = e.node; + }).bind(this)); + }, + + /** + * Sets the heap dumps and schedules rebuilding the pane. + * + * The provided value should be a chronological list of heap dumps. All + * dumps are assumed to belong to the same process and belong to the same + * allocator. Example: + * + * [ + * tr.model.HeapDump {}, // Heap dump at timestamp 1. + * undefined, // Heap dump not provided at timestamp 2. + * tr.model.HeapDump {}, // Heap dump at timestamp 3. + * ] + */ + set heapDumps(heapDumps) { + this.heapDumps_ = heapDumps; + this.scheduleRebuild_(); + }, + + get heapDumps() { + return this.heapDumps_; + }, + + set aggregationMode(aggregationMode) { + this.aggregationMode_ = aggregationMode; + this.$.path_view.aggregationMode = aggregationMode; + this.$.breakdown_view.aggregationMode = aggregationMode; + }, + + get aggregationMode() { + return this.aggregationMode_; + }, + + set viewMode(viewMode) { + this.viewMode_ = viewMode; + this.scheduleRebuild_(); + }, + + get viewMode() { + return this.viewMode_; + }, + + get heavyView() { + switch (this.viewMode) { + case MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW: + case MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW: + return true; + default: + return false; + } + }, + + onRebuild_() { + if (this.heapDumps_ === undefined || + this.heapDumps_.length === 0) { + // Show the info text (hide the table and the view mode selector). + this.$.info_text.style.display = 'block'; + this.$.split_view.style.display = 'none'; + this.$.view_mode_container.style.display = 'none'; + this.$.info_bar.hidden = true; + this.$.path_view.selectedNode = undefined; + return; + } + + // Show the table and the view mode selector (hide the info text). + this.$.info_text.style.display = 'none'; + this.$.split_view.style.display = 'flex'; + this.$.view_mode_container.style.display = 'block'; + + // Show the info bar if in heavy view mode. + this.$.info_bar.hidden = !this.heavyView; + + this.$.path_view.selectedNode = this.createHeapTree_(); + this.$.path_view.rebuild(); + this.$.breakdown_view.rebuild(); + }, + + createHeapTree_() { + const definedHeapDump = this.heapDumps_.find(x => x); + if (definedHeapDump === undefined) return undefined; + + // The title of the root node is the name of the allocator. + const rootRowTitle = definedHeapDump.allocatorName; + + const stackFrameTrees = this.createStackFrameTrees_(this.heapDumps_); + + return new HeapDumpTreeNode(stackFrameTrees, + tr.ui.analysis.HeapDetailsRowDimension.ROOT, rootRowTitle, + this.heavyView); + }, + + createStackFrameTrees_(heapDumps) { + const builders = heapDumps.map(heapDump => this.createBuilder_(heapDump)); + const views = builders.map(builder => { + if (builder === undefined) return undefined; + return builder.buildView(this.viewMode); + }); + return views; + }, + + createBuilder_(heapDump) { + if (heapDump === undefined) return undefined; + + if (this.cachedBuilders_.has(heapDump)) { + return this.cachedBuilders_.get(heapDump); + } + + const dimensions = 2; // stack frames, object type + const valueCount = 2; // size, count + const builder = new MultiDimensionalViewBuilder(dimensions, valueCount); + + // Build the heap tree. + for (const entry of heapDump.entries) { + const leafStackFrame = entry.leafStackFrame; + const stackTracePath = leafStackFrame === undefined ? + [] : leafStackFrame.getUserFriendlyStackTrace().reverse(); + + const objectTypeName = entry.objectTypeName; + const objectTypeNamePath = objectTypeName === undefined ? + [] : [objectTypeName]; + + const valueKind = entry.valuesAreTotals ? + MultiDimensionalViewBuilder.ValueKind.TOTAL : + MultiDimensionalViewBuilder.ValueKind.SELF; + + builder.addPath([stackTracePath, objectTypeNamePath], + [entry.size, entry.count], + valueKind); + } + + builder.complete = heapDump.isComplete; + this.cachedBuilders_.set(heapDump, builder); + return builder; + }, + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane_test.html new file mode 100644 index 00000000000..947cc532e7a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane_test.html @@ -0,0 +1,4045 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel='import' href='/tracing/base/multi_dimensional_view.html'> +<link rel='import' href='/tracing/base/unit.html'> +<link rel='import' href='/tracing/base/utils.html'> +<link rel='import' href='/tracing/core/test_utils.html'> +<link rel='import' href='/tracing/model/heap_dump.html'> +<link rel='import' href='/tracing/model/memory_dump_test_utils.html'> +<link rel='import' + href='/tracing/ui/analysis/memory_dump_heap_details_pane.html'> +<link rel='import' + href='/tracing/ui/analysis/memory_dump_sub_view_test_utils.html'> +<link rel='import' href='/tracing/ui/analysis/memory_dump_sub_view_util.html'> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ViewType = tr.b.MultiDimensionalViewBuilder.ViewType; + const TOP_DOWN_TREE_VIEW = ViewType.TOP_DOWN_TREE_VIEW; + const TOP_DOWN_HEAVY_VIEW = ViewType.TOP_DOWN_HEAVY_VIEW; + const BOTTOM_UP_HEAVY_VIEW = ViewType.BOTTOM_UP_HEAVY_VIEW; + const HeapDump = tr.model.HeapDump; + const HeapDetailsRowDimension = tr.ui.analysis.HeapDetailsRowDimension; + const ROOT = HeapDetailsRowDimension.ROOT; + const STACK_FRAME = HeapDetailsRowDimension.STACK_FRAME; + const OBJECT_TYPE = HeapDetailsRowDimension.OBJECT_TYPE; + const TitleColumn = tr.ui.analysis.TitleColumn; + const NumericMemoryColumn = tr.ui.analysis.NumericMemoryColumn; + const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode; + const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump; + const addProcessMemoryDump = + tr.model.MemoryDumpTestUtils.addProcessMemoryDump; + const checkColumns = tr.ui.analysis.checkColumns; + const checkNumericFields = tr.ui.analysis.checkNumericFields; + const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields; + const isElementDisplayed = tr.ui.analysis.isElementDisplayed; + const count_smallerIsBetter = tr.b.Unit.byName.count_smallerIsBetter; + + function createHeapDumps(withCount) { + const model = new tr.Model(); + const process = model.getOrCreateProcess(1); + + function addHeapEntry(heapDump, stackFrames, objectTypeName, size, count) { + const leafStackFrame = stackFrames === undefined ? undefined : + tr.c.TestUtils.newStackTrace(model, stackFrames); + heapDump.addEntry(leafStackFrame, objectTypeName, size, + withCount ? count : undefined); + } + + // First timestamp. + const gmd1 = addGlobalMemoryDump(model, {ts: -10}); + const pmd1 = addProcessMemoryDump(gmd1, process, {ts: -11}); + const hd1 = new HeapDump(pmd1, 'partition_alloc'); + + addHeapEntry(hd1, undefined /* sum over all traces */, + undefined /* sum over all types */, 4194304 /* 4 MiB */, 1000); + addHeapEntry(hd1, undefined /* sum over all traces */, 'v8::Context', + 1048576 /* 1 MiB */, 200); + addHeapEntry(hd1, undefined /* sum over all traces */, 'blink::Node', + 331776 /* 324 KiB */, 10); + addHeapEntry(hd1, ['MessageLoop::RunTask'], + undefined /* sum over all types */, 4194304 /* 4 MiB */, 1000); + addHeapEntry(hd1, ['MessageLoop::RunTask'], 'v8::Context', + 1048576 /* 1 MiB */, 200); + + addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall'], + undefined /* sum over all types */, 1406976 /* 1.3 MiB */, 299); + addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall'], + 'blink::Node', 331776 /* 324 KiB */, 10); + addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall'], 'v8::Context', + 1024000 /* 1000 KiB */, 176); + addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', '<self>'], + undefined /* sum over all types */, 102400 /* 100 KiB */, 30); + addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'], + 'v8::Context', 716800 /* 700 KiB */, 100); + addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'], + undefined /* sum over all types */, 1048576 /* 1 MiB */, 101); + addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall'], + undefined /* sum over all types */, + 153600 /* 150 KiB, lower than the actual sum (should be ignored) */, + 25 /* the allocation count should, however, NOT be ignored */); + addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall'], + 'v8::Context', 153600 /* 150 KiB */, 15); + + // The following entry should not appear in the tree-view because there is + // no entry for its parent stack frame. + addHeapEntry(hd1, ['MessageLoop::RunTask', 'MissingParent', 'FunctionCall'], + undefined /* sum over all types */, 10 /* 10 B */, 2); + + // The following entry should not appear in the tree-view because there is + // no sum over all types (for the given stack trace). However, it will lead + // to a visible increase of the (incorrectly provided) sum over all types + // of MessageLoop::RunTask -> FunctionCall -> FunctionCall by 50 KiB. + addHeapEntry(hd1, + ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall', + 'FunctionCall'], + 'MissingSumOverAllTypes', 51200 /* 50 KiB */, 9); + + addHeapEntry(hd1, ['MessageLoop::RunTask', 'V8.Execute'], + undefined /* sum over all types */, 2404352 /* 2.3 MiB */, 399); + addHeapEntry(hd1, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'], + undefined /* sum over all types */, 2404352 /* 2.3 MiB */, 399); + addHeapEntry(hd1, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'], + 'v8::Context', 20480 /* 20 KiB */, 6); + addHeapEntry(hd1, + ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', '<self>'], + 'v8::Context', 15360 /* 15 KiB */, 5); + addHeapEntry(hd1, + ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', 'V8.Execute'], + undefined /* sum over all types */, 2097152 /* 2 MiB */, 99); + addHeapEntry(hd1, + ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', 'V8.Execute', + 'V8.Execute'], + undefined /* sum over all types */, 2097152 /* 2 MiB */, 99); + addHeapEntry(hd1, + ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', '<self>'], + undefined /* sum over all types */, 307200 /* 300 KiB */, 300); + + // Second timestamp. + const gmd2 = addGlobalMemoryDump(model, {ts: 10}); + const pmd2 = addProcessMemoryDump(gmd2, process, {ts: 11}); + const hd2 = new HeapDump(pmd2, 'partition_alloc'); + + addHeapEntry(hd2, undefined /* sum over all traces */, + undefined /* sum over all types */, + 3145728 /* 3 MiB, lower than the actual sum (should be ignored) */, + 900 /* the allocation count should, however, NOT be ignored */); + addHeapEntry(hd2, undefined /* sum over all traces */, + 'v8::Context', 1258291 /* 1.2 MiB */, 520); + addHeapEntry(hd2, undefined /* sum over all traces */, + 'blink::Node', 1048576 /* 1 MiB */, 5); + addHeapEntry(hd2, ['<self>'], undefined /* sum over all types */, + 131072 /* 128 KiB */, 16); + addHeapEntry(hd2, ['<self>'], 'v8::Context', 131072 /* 128 KiB */, 16); + addHeapEntry(hd2, ['MessageLoop::RunTask'], + undefined /* sum over all types */, 4823449 /* 4.6 MiB */, 884); + addHeapEntry(hd2, ['MessageLoop::RunTask'], 'v8::Context', + 1127219 /* 1.1 MiB */, 317); + + addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall'], + undefined /* sum over all types */, 2170880 /* 2.1 MiB */, 600); + addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall'], 'v8::Context', + 1024000 /* 1000 KiB */, 500); + addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall'], 'blink::Node', + 819200 /* 800 KiB */, 4); + addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'], + undefined /* sum over all types */, 1572864 /* 1.5 MiB */, 270); + addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'], + 'v8::Context', 614400 /* 600 KiB */, 123); + addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'], + 'blink::Node', 819200 /* 800 KiB */, 4); + addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall'], + undefined /* sum over all types */, 204800 /* 200 KiB */, 313); + addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall'], + 'v8::Context', 122880 /* 120 KiB */, 270); + addHeapEntry(hd2, + ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall', + 'FunctionCall'], + undefined /* sum over all types */, 204800 /* 200 KiB */, 313); + addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', '<self>'], + undefined /* sum over all types */, 393216 /* 384 KiB */, 17); + + addHeapEntry(hd2, ['MessageLoop::RunTask', 'V8.Execute'], + undefined /* sum over all types */, 2621440 /* 2.5 MiB */, 199); + addHeapEntry(hd2, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'], + undefined /* sum over all types */, 2621440 /* 2.5 MiB */, 199); + addHeapEntry(hd2, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'], + 'v8::Context', 20480 /* 20 KiB */, 4); + addHeapEntry(hd2, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'], + 'WTF::StringImpl', 126362 /* 123.4 KiB */, 56); + addHeapEntry(hd2, + ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', 'V8.Execute'], + undefined /* sum over all types */, 2516582 /* 2.4 MiB */, 158); + + return [hd1, hd2]; + } + + function createSelfHeapDumps(withCount) { + const model = new tr.Model(); + const process = model.getOrCreateProcess(1); + + function addHeapEntry(heapDump, stackFrames, objectTypeName, size, count) { + const leafStackFrame = stackFrames === undefined ? undefined : + tr.c.TestUtils.newStackTrace(model, stackFrames); + heapDump.addEntry(leafStackFrame, objectTypeName, size, + withCount ? count : undefined, false /* valuesAreTotals */); + } + + // First timestamp. + const gmd1 = addGlobalMemoryDump(model, {ts: -10}); + const pmd1 = addProcessMemoryDump(gmd1, process, {ts: -11}); + const hd1 = new HeapDump(pmd1, 'partition_alloc'); + hd1.isComplete = true; + + addHeapEntry(hd1, ['MessageLoop::RunTask', 'a', 'AllocSomething'], + 'v8::Context', 1024, 100); + addHeapEntry(hd1, ['MessageLoop::RunTask', 'a', 'b', 'AllocSomething'], + 'v8::Context', 1024, 100); + addHeapEntry(hd1, ['MessageLoop::RunTask', 'a', 'b', 'c', 'AllocSomething'], + 'v8::Context', 1024, 100); + + return [hd1]; + } + + + function checkDisplayedElements(viewEl, displayExpectations) { + assert.strictEqual(isElementDisplayed(viewEl.$.info_text), + displayExpectations.infoText); + assert.strictEqual(isElementDisplayed(viewEl.$.info_bar), + displayExpectations.infoBar); + assert.strictEqual(isElementDisplayed(viewEl.$.split_view), + displayExpectations.tableAndSplitView); + assert.strictEqual(isElementDisplayed(viewEl.$.view_mode_container), + displayExpectations.tableAndSplitView); + } + + const EXPECTED_COLUMNS_WITHOUT_COUNT = [ + { title: 'Current path', type: TitleColumn, noAggregation: true }, + { title: 'Size', type: NumericMemoryColumn } + ]; + + const EXPECTED_COLUMNS_WITH_COUNT = EXPECTED_COLUMNS_WITHOUT_COUNT.concat([ + { title: 'Count', type: NumericMemoryColumn }, + ]); + + const EXPECTED_CELLS = ['Size', 'Count']; + + function checkNode(node, expectedNodeStructure, expectedParentNode) { + assert.strictEqual(node.title, expectedNodeStructure.title); + assert.strictEqual(node.dimension, expectedNodeStructure.dimension); + assert.strictEqual(node.parentNode, expectedParentNode); + + // Check that there AREN'T any cells that we are NOT expecting. + const cells = node.cells; + assert.includeMembers(EXPECTED_CELLS, Object.keys(cells)); + + const sizeCell = cells.Size; + const sizeFields = sizeCell ? sizeCell.fields : undefined; + checkSizeNumericFields(sizeFields, undefined, expectedNodeStructure.size); + + const countCell = cells.Count; + const countFields = countCell ? countCell.fields : undefined; + checkNumericFields(countFields, undefined, expectedNodeStructure.count, + count_smallerIsBetter); + + assert.strictEqual(node.childNodes.size, 2); + + // If |expectedNodeStructure.children| is undefined, check that there are + // no child nodes. + if (!expectedNodeStructure.children) { + assert.lengthOf(node.childNodes.get(STACK_FRAME), 0); + assert.lengthOf(node.childNodes.get(OBJECT_TYPE), 0); + return; + } + + // If |expectedNodeStructure.children| is just a number, check total number + // of child nodes. + if (typeof expectedNodeStructure.children === 'number') { + assert.strictEqual(expectedNodeStructure.children, + node.childNodes.get(STACK_FRAME).length + + node.childNodes.get(OBJECT_TYPE).length); + return; + } + + // Check child nodes wrt both dimensions. + checkNodes(node.childNodes.get(STACK_FRAME), + expectedNodeStructure.children.filter(c => c.dimension === STACK_FRAME), + node); + checkNodes(node.childNodes.get(OBJECT_TYPE), + expectedNodeStructure.children.filter(c => c.dimension === OBJECT_TYPE), + node); + } + + function checkNodes(nodes, expectedStructure, expectedParentNode) { + assert.lengthOf(nodes, expectedStructure.length); + for (let i = 0; i < expectedStructure.length; i++) { + checkNode(nodes[i], expectedStructure[i], expectedParentNode); + } + } + + function checkSplitView(viewEl, expectedConfig, expectedStructure) { + checkDisplayedElements(viewEl, { + infoText: false, + tableAndSplitView: true, + infoBar: !!expectedConfig.expectedInfoBarDisplayed + }); + + // Both the split view and breakdown view should be displaying the same + // node. + const selectedNode = viewEl.$.path_view.selectedNode; + assert.strictEqual(viewEl.$.breakdown_view.displayedNode, selectedNode); + checkNodes([selectedNode], expectedStructure, + undefined /* expectedParentNode */); + + // TODO: Add proper tests for tr-ui-a-memory-dump-heap-details-path-view + // and tr-ui-a-memory-dump-heap-details-breakdown-view. + const expectedColumns = expectedConfig.expectedCountColumns ? + EXPECTED_COLUMNS_WITH_COUNT : EXPECTED_COLUMNS_WITHOUT_COUNT; + checkColumns(viewEl.$.path_view.$.table.tableColumns, expectedColumns, + expectedConfig.expectedAggregationMode); + } + + function changeView(viewEl, viewType) { + tr.ui.b.findDeepElementMatching(viewEl, 'select').selectedValue = viewType; + viewEl.rebuild(); + } + + test('instantiate_empty', function() { + tr.ui.analysis.createAndCheckEmptyPanes(this, + 'tr-ui-a-memory-dump-heap-details-pane', 'heapDumps', + function(viewEl) { + // Check that the info text is shown. + checkDisplayedElements(viewEl, { + infoText: true, + tableAndSplitView: false, + infoBar: false + }); + }); + }); + + test('instantiate_noEntries', function() { + const heapDumps = createHeapDumps(false).slice(0, 1); + heapDumps[0].entries = []; + + const viewEl = tr.ui.analysis.createTestPane( + 'tr-ui-a-memory-dump-heap-details-pane'); + viewEl.heapDumps = heapDumps; + viewEl.rebuild(); + this.addHTMLOutput(viewEl); + + // Top-down tree view (default). + checkSplitView(viewEl, + { /* empty expectedConfig */ }, + [ + { + dimension: ROOT, + title: 'partition_alloc', + size: [0], + defined: [true] + } + ]); + + changeView(viewEl, TOP_DOWN_HEAVY_VIEW); + checkSplitView(viewEl, + { expectedInfoBarDisplayed: true }, + [ + { + dimension: ROOT, + title: 'partition_alloc', + size: [0], + defined: [true] + } + ]); + + changeView(viewEl, BOTTOM_UP_HEAVY_VIEW); + checkSplitView(viewEl, + { expectedInfoBarDisplayed: true }, + [ + { + dimension: ROOT, + title: 'partition_alloc', + size: [0], + defined: [true] + } + ]); + + changeView(viewEl, TOP_DOWN_TREE_VIEW); + }); + + test('instantiate_single', function() { + const heapDumps = createHeapDumps(false).slice(0, 1); + + const viewEl = tr.ui.analysis.createTestPane( + 'tr-ui-a-memory-dump-heap-details-pane'); + viewEl.heapDumps = heapDumps; + viewEl.rebuild(); + this.addHTMLOutput(viewEl); + + // Top-down tree view (default). + checkSplitView(viewEl, + { /* empty expectedConfig */ }, + [ + { + dimension: ROOT, + title: 'partition_alloc', + size: [4194304], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [4194304], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1406976], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [102400], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [1048576], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [716800], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [331776], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600 + 51200], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [153600], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [51200], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [51200], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1024000], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [153600], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [51200], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2404352], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [2404352], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [307200], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [291840], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [5120], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [2383872], + defined: [true], + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [382976], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1048576], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1024000], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [153600], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [24576], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [3145728], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1048576], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [1048576], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1024000], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [153600], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [24576], + defined: [true], + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [2813952], + defined: [true], + } + ] + } + ]); + + changeView(viewEl, BOTTOM_UP_HEAVY_VIEW); + checkSplitView(viewEl, + { expectedInfoBarDisplayed: true }, + [ + { + dimension: ROOT, + title: 'partition_alloc', + size: [4194304], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [4194304], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1048576], + defined: [true] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776], + defined: [true] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [3811338], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [1406976], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776], + defined: [true] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1024000], + defined: [true] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [204800], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [204800], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [153600], + defined: [true] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200], + defined: [true] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [153600], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [153600], + defined: [true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200], + defined: [true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200], + defined: [true] + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'MissingParent', + size: [10], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [10], + defined: [true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2404352], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [2404352], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [20480], + defined: [true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [20480], + defined: [true] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [331776], + defined: [true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1044480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [1024000], + defined: [true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [153600], + defined: [true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [20480], + defined: [true] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200], + defined: [true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200], + defined: [true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200], + defined: [true] + } + ] + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<self>', + size: [409600], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [409600], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [102400], + defined: [true] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [307200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [307200], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360], + defined: [true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [15360], + defined: [true] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [15360], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [15360], + defined: [true] + } + ] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [15360], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [15360], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [15360], + defined: [true] + } + ] + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [3452928], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [3145728], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [1048576], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [716800], + defined: [true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [2097152], + defined: [true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [716800], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [716800], + defined: [true] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [2404352], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [20480], + defined: [true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [2097152], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [2097152], + defined: [true] + } + ] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [737280], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [716800], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [716800], + defined: [true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [20480], + defined: [true] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'MissingParent', + size: [10], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [10], + defined: [true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1048576], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [1048576], + defined: [true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1044480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [1024000], + defined: [true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [153600], + defined: [true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [20480], + defined: [true] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [15360], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [15360], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [15360], + defined: [true] + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [737280], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [716800], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [716800], + defined: [true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [20480], + defined: [true] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [331776], + defined: [true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [331776], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [331776], + defined: [true] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200], + defined: [true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200], + defined: [true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200], + defined: [true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200], + defined: [true] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ]); + + changeView(viewEl, TOP_DOWN_HEAVY_VIEW); + checkSplitView(viewEl, + { expectedInfoBarDisplayed: true }, [ + { + dimension: ROOT, + title: 'partition_alloc', + size: [4194304], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [4194304], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1406976], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [102400], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [1048576], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [716800], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600 + 51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [153600], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1024000], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'MissingParent', + size: [10], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [10], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2404352], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [2404352], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [307200], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360], + defined: [true], + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360], + defined: [true], + } + ] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1048576], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1024000], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360], + defined: [true], + } + ] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [331776], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + } + ] + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1406976 + 10 + 2404352], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [102400 + 307200], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [1048576 + 2097152], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [716800], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600 + 51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [153600], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1024000 + 20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<self>', + size: [102400 + 307200], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [1048576 + 2404352], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [2404352], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [307200], + defined: [true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360], + defined: [true], + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152], + defined: [true], + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [716800 + 20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360], + defined: [true], + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'MissingParent', + size: [10], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [10], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1048576], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [1048576], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1024000], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360], + defined: [true], + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1024000 + 20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360], + defined: [true], + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800 + 20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [20480], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360], + defined: [true], + } + ] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [331776], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [331776], + defined: [true], + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [331776], + defined: [true], + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200], + defined: [true], + } + ] + } + ] + } + ] + } + ] + } + ]); + }); + + test('instantiate_multipleDiff', function() { + const heapDumps = createHeapDumps(true /* with allocation counts */); + + const viewEl = tr.ui.analysis.createTestPane( + 'tr-ui-a-memory-dump-heap-details-pane'); + viewEl.heapDumps = heapDumps; + viewEl.aggregationMode = AggregationMode.DIFF; + viewEl.rebuild(); + this.addHTMLOutput(viewEl); + + changeView(viewEl, TOP_DOWN_HEAVY_VIEW); + checkSplitView(viewEl, + { + expectedAggregationMode: AggregationMode.DIFF, + expectedInfoBarDisplayed: true, + expectedCountColumns: true + }, + [ + { + dimension: ROOT, + title: 'partition_alloc', + size: [4194304, 4954521], + count: [1000, 900], + averageSize: [4194304 / 1000, 4954521 / 900], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [4194304, 4823449], + count: [1000, 884], + averageSize: [4194304 / 1000, 4823449 / 884], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1406976, 2170880], + count: [299, 600], + averageSize: [1406976 / 299, 2170880 / 600], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [102400, 393216], + count: [30, 17], + averageSize: [102400 / 30, 393216 / 17], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [1048576, 1572864], + count: [101, 270], + averageSize: [1048576 / 101, 1572864 / 270], + defined: [true, true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [716800, 614400], + count: [100, 123], + averageSize: [716800 / 100, 614400 / 123], + defined: [true, true] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [undefined, 819200], + count: [undefined, 4], + averageSize: [undefined, 819200 / 4], + defined: [false, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [204800, 204800], + count: [25, 313], + averageSize: [204800 / 25, 204800 / 313], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, 204800], + count: [9, 313], + averageSize: [51200 / 9, 204800 / 313], + defined: [true, true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [153600, 122880], + count: [15, 270], + averageSize: [153600 / 15, 122880 / 270], + defined: [true, true] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776, 819200], + count: [10, 4], + averageSize: [331776 / 10, 819200 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [undefined, 819200], + count: [undefined, 4], + averageSize: [undefined, 819200 / 4], + defined: [false, true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1024000, 1024000], + count: [176, 500], + averageSize: [1024000 / 176, 1024000 / 500], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800, 614400], + count: [100, 123], + averageSize: [716800 / 100, 614400 / 123], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600, 122880], + count: [15, 270], + averageSize: [153600 / 15, 122880 / 270], + defined: [true, true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false] + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'MissingParent', + size: [10, undefined], + count: [2, undefined], + averageSize: [10 / 2, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [10, undefined], + count: [2, undefined], + averageSize: [10 / 2, undefined], + defined: [true, false] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2404352, 2621440], + count: [399, 199], + averageSize: [2404352 / 399, 2621440 / 199], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [2404352, 2621440], + count: [399, 199], + averageSize: [2404352 / 399, 2621440 / 199], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [307200, undefined], + count: [300, undefined], + averageSize: [307200 / 300, undefined], + defined: [true, false], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152, 2516582], + count: [99, 158], + averageSize: [2097152 / 99, 2516582 / 158], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152, undefined], + count: [99, undefined], + averageSize: [2097152 / 99, undefined], + defined: [true, false] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [20480, 20480], + count: [6, 4], + averageSize: [20480 / 6, 20480 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'WTF::StringImpl', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [20480, 20480], + count: [6, 4], + averageSize: [20480 / 6, 20480 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [20480, 20480], + count: [6, 4], + averageSize: [20480 / 6, 20480 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'WTF::StringImpl', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1048576, 1127219], + count: [200, 504], + averageSize: [1048576 / 200, 1127219 / 504], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1024000, 1024000], + count: [176, 500], + averageSize: [1024000 / 176, 1024000 / 500], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800, 614400], + count: [100, 123], + averageSize: [716800 / 100, 614400 / 123], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600, 122880], + count: [15, 270], + averageSize: [153600 / 15, 122880 / 270], + defined: [true, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [20480, 20480], + count: [6, 4], + averageSize: [20480 / 6, 20480 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [20480, 20480], + count: [6, 4], + averageSize: [20480 / 6, 20480 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + } + ] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776, 819200], + count: [10, 4], + averageSize: [331776 / 10, 819200 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [331776, 819200], + count: [10, 4], + averageSize: [331776 / 10, 819200 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [undefined, 819200], + count: [undefined, 4], + averageSize: [undefined, 819200 / 4], + defined: [false, true] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false] + } + ] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'WTF::StringImpl', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true] + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [3811338, 4792320], + count: [700, 799], + averageSize: [3811338 / 700, 4792320 / 799], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [409600, 393216], + count: [330, 17], + averageSize: [409600 / 330, 393216 / 17], + defined: [true, true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [3145728, 4089446], + count: [200, 428], + averageSize: [3145728 / 200, 4089446 / 428], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152, undefined], + count: [99, undefined], + averageSize: [2097152 / 99, undefined], + defined: [true, false] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [716800, 614400], + count: [100, 123], + averageSize: [716800 / 100, 614400 / 123], + defined: [true, true] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [undefined, 819200], + count: [undefined, 4], + averageSize: [undefined, 819200 / 4], + defined: [false, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [204800, 204800], + count: [25, 313], + averageSize: [204800 / 25, 204800 / 313], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, 204800], + count: [9, 313], + averageSize: [51200 / 9, 204800 / 313], + defined: [true, true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [153600, 122880], + count: [15, 270], + averageSize: [153600 / 15, 122880 / 270], + defined: [true, true] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776, 819200], + count: [10, 4], + averageSize: [331776 / 10, 819200 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [undefined, 819200], + count: [undefined, 4], + averageSize: [undefined, 819200 / 4], + defined: [false, true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1044480, 1044480], + count: [182, 504], + averageSize: [1044480 / 182, 1044480 / 504], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800, 614400], + count: [100, 123], + averageSize: [716800 / 100, 614400 / 123], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600, 122880], + count: [15, 270], + averageSize: [153600 / 15, 122880 / 270], + defined: [true, true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'WTF::StringImpl', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<self>', + size: [409600, 524288], + count: [330, 33], + averageSize: [409600 / 330, 524288 / 33], + defined: [true, true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360, 131072], + count: [5, 16], + averageSize: [15360 / 5, 131072 / 16], + defined: [true, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [3452928, 4194304], + count: [500, 469], + averageSize: [3452928 / 500, 4194304 / 469], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [2404352, 2621440], + count: [399, 199], + averageSize: [2404352 / 399, 2621440 / 199], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [307200, undefined], + count: [300, undefined], + averageSize: [307200 / 300, undefined], + defined: [true, false], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152, 2516582], + count: [99, 158], + averageSize: [2097152 / 99, 2516582 / 158], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152, undefined], + count: [99, undefined], + averageSize: [2097152 / 99, undefined], + defined: [true, false] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [20480, 20480], + count: [6, 4], + averageSize: [20480 / 6, 20480 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'WTF::StringImpl', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152, undefined], + count: [99, undefined], + averageSize: [2097152 / 99, undefined], + defined: [true, false] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [737280, 634880], + count: [106, 127], + averageSize: [737280 / 106, 634880 / 127], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [20480, 20480], + count: [6, 4], + averageSize: [20480 / 6, 20480 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [undefined, 819200], + count: [undefined, 4], + averageSize: [undefined, 819200 / 4], + defined: [false, true] + }, + { + dimension: OBJECT_TYPE, + title: 'WTF::StringImpl', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'MissingParent', + size: [10, undefined], + count: [2, undefined], + averageSize: [10 / 2, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [10, undefined], + count: [2, undefined], + averageSize: [10 / 2, undefined], + defined: [true, false] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1048576, 1258291], + count: [200, 520], + averageSize: [1048576 / 200, 1258291 / 520], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [1048576, 1127219], + count: [200, 504], + averageSize: [1048576 / 200, 1127219 / 504], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1024000, 1024000], + count: [176, 500], + averageSize: [1024000 / 176, 1024000 / 500], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800, 614400], + count: [100, 123], + averageSize: [716800 / 100, 614400 / 123], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600, 122880], + count: [15, 270], + averageSize: [153600 / 15, 122880 / 270], + defined: [true, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [20480, 20480], + count: [6, 4], + averageSize: [20480 / 6, 20480 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [20480, 20480], + count: [6, 4], + averageSize: [20480 / 6, 20480 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1044480, 1044480], + count: [182, 504], + averageSize: [1044480 / 182, 1044480 / 504], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800, 614400], + count: [100, 123], + averageSize: [716800 / 100, 614400 / 123], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600, 122880], + count: [15, 270], + averageSize: [153600 / 15, 122880 / 270], + defined: [true, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360, 131072], + count: [5, 16], + averageSize: [15360 / 5, 131072 / 16], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [737280, 634880], + count: [106, 127], + averageSize: [737280 / 106, 634880 / 127], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [20480, 20480], + count: [6, 4], + averageSize: [20480 / 6, 20480 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + } + ] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776, 1048576], + count: [10, 5], + averageSize: [331776 / 10, 1048576 / 5], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [331776, 819200], + count: [10, 4], + averageSize: [331776 / 10, 819200 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [331776, 819200], + count: [10, 4], + averageSize: [331776 / 10, 819200 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [undefined, 819200], + count: [undefined, 4], + averageSize: [undefined, 819200 / 4], + defined: [false, true] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [331776, 819200], + count: [10, 4], + averageSize: [331776 / 10, 819200 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [undefined, 819200], + count: [undefined, 4], + averageSize: [undefined, 819200 / 4], + defined: [false, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [undefined, 819200], + count: [undefined, 4], + averageSize: [undefined, 819200 / 4], + defined: [false, true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'MissingSumOverAllTypes', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false] + } + ] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [51200, undefined], + count: [9, undefined], + averageSize: [51200 / 9, undefined], + defined: [true, false] + } + ] + } + ] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'WTF::StringImpl', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true] + } + ] + } + ] + } + ] + } + ]); + + changeView(viewEl, TOP_DOWN_TREE_VIEW); + checkSplitView(viewEl, + { + expectedAggregationMode: AggregationMode.DIFF, + expectedCountColumns: true + }, + [ + { + dimension: ROOT, + title: 'partition_alloc', + size: [4194304, 4954521], + count: [1000, 900], + averageSize: [4194304 / 1000, 4954521 / 900], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [4194304, 4823449], + count: [1000, 884], + averageSize: [4194304 / 1000, 4823449 / 884], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1406976, 2170880], + count: [299, 600], + averageSize: [1406976 / 299, 2170880 / 600], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [102400, 393216], + count: [30, 17], + averageSize: [102400 / 30, 393216 / 17], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [1048576, 1572864], + count: [101, 270], + averageSize: [1048576 / 101, 1572864 / 270], + defined: [true, true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [716800, 614400], + count: [100, 123], + averageSize: [716800 / 100, 614400 / 123], + defined: [true, true] + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [331776, 139264], + count: [1, 143], + averageSize: [331776 / 1, 139264 / 143], + defined: [true, true] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [undefined, 819200], + count: [undefined, 4], + averageSize: [undefined, 819200 / 4], + defined: [false, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [204800, 204800], + count: [25, 313], + averageSize: [204800 / 25, 204800 / 313], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [undefined, 204800], + count: [undefined, 313], + averageSize: [undefined, 204800 / 313], + defined: [false, true] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [153600, 122880], + count: [15, 270], + averageSize: [153600 / 15, 122880 / 270], + defined: [true, true] + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [51200, 81920], + count: [10, 43], + averageSize: [51200 / 10, 81920 / 43], + defined: [true, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [51200, undefined], + count: [143, undefined], + averageSize: [51200 / 143, undefined], + defined: [true, false] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776, 819200], + count: [10, 4], + averageSize: [331776 / 10, 819200 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [undefined, 819200], + count: [undefined, 4], + averageSize: [undefined, 819200 / 4], + defined: [false, true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1024000, 1024000], + count: [176, 500], + averageSize: [1024000 / 176, 1024000 / 500], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800, 614400], + count: [100, 123], + averageSize: [716800 / 100, 614400 / 123], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600, 122880], + count: [15, 270], + averageSize: [153600 / 15, 122880 / 270], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [153600, 286720], + count: [61, 107], + averageSize: [153600 / 61, 286720 / 107], + defined: [true, true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [51200, 327680], + count: [113, 96], + averageSize: [51200 / 113, 327680 / 96], + defined: [true, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2404352, 2621440], + count: [399, 199], + averageSize: [2404352 / 399, 2621440 / 199], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [2404352, 2621440], + count: [399, 199], + averageSize: [2404352 / 399, 2621440 / 199], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [307200, undefined], + count: [300, undefined], + averageSize: [307200 / 300, undefined], + defined: [true, false], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [291840, undefined], + count: [295, undefined], + averageSize: [291840 / 295, undefined], + defined: [true, false] + } + ] + }, + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152, 2516582], + count: [99, 158], + averageSize: [2097152 / 99, 2516582 / 158], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [2097152, undefined], + count: [99, undefined], + averageSize: [2097152 / 99, undefined], + defined: [true, false] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [undefined, 104858], + count: [undefined, 41], + averageSize: [undefined, 104858 / 41], + defined: [false, true] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [20480, 20480], + count: [6, 4], + averageSize: [20480 / 6, 20480 / 4], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: '<self>', + size: [15360, undefined], + count: [5, undefined], + averageSize: [15360 / 5, undefined], + defined: [true, false] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [5120, undefined], + count: [1, undefined], + averageSize: [5120 / 1, undefined], + defined: [true, false] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [2383872, 2474598], + count: [393, 139], + averageSize: [2383872 / 393, 2474598 / 139], + defined: [true, true] + }, + { + dimension: OBJECT_TYPE, + title: 'WTF::StringImpl', + size: [undefined, 126362], + count: [undefined, 56], + averageSize: [undefined, 126362 / 56], + defined: [false, true] + } + ] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [382976, 31129], + count: [302, 85], + averageSize: [382976 / 302, 31129 / 85], + defined: [true, true] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1048576, 1127219], + count: [200, 504], + averageSize: [1048576 / 200, 1127219 / 504], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1024000, 1024000], + count: [176, 500], + averageSize: [1024000 / 176, 1024000 / 500], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800, 614400], + count: [100, 123], + averageSize: [716800 / 100, 614400 / 123], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600, 122880], + count: [15, 270], + averageSize: [153600 / 15, 122880 / 270], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [153600, 286720], + count: [61, 107], + averageSize: [153600 / 61, 286720 / 107], + defined: [true, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [24576, 103219], + count: [24, 4], + averageSize: [24576 / 24, 103219 / 4], + defined: [true, true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [3145728, 3696230], + count: [800, 380], + averageSize: [3145728 / 800, 3696230 / 380], + defined: [true, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<self>', + size: [undefined, 131072], + count: [undefined, 16], + averageSize: [undefined, 131072 / 16], + defined: [false, true], + children: [ + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [undefined, 131072], + count: [undefined, 16], + averageSize: [undefined, 131072 / 16], + defined: [false, true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'v8::Context', + size: [1048576, 1258291], + count: [200, 520], + averageSize: [1048576 / 200, 1258291 / 520], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'MessageLoop::RunTask', + size: [1048576, 1127219], + count: [200, 504], + averageSize: [1048576 / 200, 1127219 / 504], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [1024000, 1024000], + count: [176, 500], + averageSize: [1024000 / 176, 1024000 / 500], + defined: [true, true], + children: [ + { + dimension: STACK_FRAME, + title: 'V8.Execute', + size: [716800, 614400], + count: [100, 123], + averageSize: [716800 / 100, 614400 / 123], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: 'FunctionCall', + size: [153600, 122880], + count: [15, 270], + averageSize: [153600 / 15, 122880 / 270], + defined: [true, true] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [153600, 286720], + count: [61, 107], + averageSize: [153600 / 61, 286720 / 107], + defined: [true, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<other>', + size: [24576, 103219], + count: [24, 4], + averageSize: [24576 / 24, 103219 / 4], + defined: [true, true] + } + ] + }, + { + dimension: STACK_FRAME, + title: '<self>', + size: [undefined, 131072], + count: [undefined, 16], + averageSize: [undefined, 131072 / 16], + defined: [false, true] + } + ] + }, + { + dimension: OBJECT_TYPE, + title: 'blink::Node', + size: [331776, 1048576], + count: [10, 5], + averageSize: [331776 / 10, 1048576 / 5], + defined: [true, true] + }, + { + dimension: OBJECT_TYPE, + title: '<other>', + size: [2813952, 2647654], + count: [790, 375], + averageSize: [2813952 / 790, 2647654 / 375], + defined: [true, true] + } + ] + } + ]); + }); + + test('instantiate_multipleMax', function() { + const heapDumps = createHeapDumps(false); + + const viewEl = tr.ui.analysis.createTestPane( + 'tr-ui-a-memory-dump-heap-details-pane'); + viewEl.heapDumps = heapDumps; + viewEl.aggregationMode = AggregationMode.MAX; + viewEl.rebuild(); + this.addHTMLOutput(viewEl); + + changeView(viewEl, TOP_DOWN_HEAVY_VIEW); + checkSplitView(viewEl, + { + expectedAggregationMode: AggregationMode.MAX, + expectedInfoBarDisplayed: true + }, + [ + { + dimension: ROOT, + title: 'partition_alloc', + size: [4194304, 4954521], + defined: [true, true], + children: 9 // No need to check the full structure again. + } + ]); + }); + + test('instantiate_multipleWithUndefined', function() { + const heapDumps = createHeapDumps(false); + heapDumps.splice(1, 0, undefined); + + const viewEl = tr.ui.analysis.createTestPane( + 'tr-ui-a-memory-dump-heap-details-pane'); + viewEl.heapDumps = heapDumps; + viewEl.aggregationMode = AggregationMode.DIFF; + viewEl.rebuild(); + this.addHTMLOutput(viewEl); + + // Top-down tree view (default). + checkSplitView(viewEl, + { expectedAggregationMode: AggregationMode.DIFF }, + [ + { + dimension: ROOT, + title: 'partition_alloc', + size: [4194304, undefined, 4954521], + defined: [true, false, true], + children: 5 // No need to check the full structure again. + } + ]); + }); + + test('instantiate_selfHeapSingle', function() { + const heapDumps = createSelfHeapDumps(true).slice(0, 1); + + const viewEl = tr.ui.analysis.createTestPane( + 'tr-ui-a-memory-dump-heap-details-pane'); + viewEl.heapDumps = heapDumps; + viewEl.rebuild(); + this.addHTMLOutput(viewEl); + + // Top-down tree view (default). + checkSplitView(viewEl, + { expectedCountColumns: true }, + [ + { + dimension: ROOT, + title: 'partition_alloc', + size: [1024 * 3], + count: [300], + defined: [true], + children: 2 // No need to check the full structure again. + } + ]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_path_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_path_view.html new file mode 100644 index 00000000000..1cc3c8d7e5f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_path_view.html @@ -0,0 +1,149 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/base/event.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_util.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> +<link rel="import" href="/tracing/ui/analysis/rebuildable_behavior.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/scalar_context_controller.html"> + +<dom-module id='tr-ui-a-memory-dump-heap-details-path-view'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + </style> + <tr-v-ui-scalar-context-controller></tr-v-ui-scalar-context-controller> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const DOWNWARDS_ARROW_WITH_TIP_RIGHTWARDS = String.fromCharCode(0x21B3); + + function HeapDetailsPathColumn(title) { + tr.ui.analysis.HeapDetailsTitleColumn.call(this, title); + } + + HeapDetailsPathColumn.prototype = { + __proto__: tr.ui.analysis.HeapDetailsTitleColumn.prototype, + + formatTitle(row) { + const title = tr.ui.analysis.HeapDetailsTitleColumn.prototype. + formatTitle.call(this, row); + if (row.dimension === tr.ui.analysis.HeapDetailsRowDimension.ROOT) { + return title; + } + + const arrowEl = document.createElement('span'); + Polymer.dom(arrowEl).textContent = DOWNWARDS_ARROW_WITH_TIP_RIGHTWARDS; + arrowEl.style.paddingRight = '2px'; + arrowEl.style.fontWeight = 'bold'; + arrowEl.style.color = tr.b.ColorScheme.getColorForReservedNameAsString( + 'heap_dump_child_node_arrow'); + + const rowEl = document.createElement('span'); + Polymer.dom(rowEl).appendChild(arrowEl); + Polymer.dom(rowEl).appendChild(tr.ui.b.asHTMLOrTextNode(title)); + return rowEl; + } + }; + + Polymer({ + is: 'tr-ui-a-memory-dump-heap-details-path-view', + behaviors: [tr.ui.analysis.RebuildableBehavior], + + created() { + this.selectedNode_ = undefined; + this.aggregationMode_ = undefined; + }, + + ready() { + this.$.table.addEventListener('selection-changed', function(event) { + this.selectedNode_ = this.$.table.selectedTableRow; + this.didSelectedNodeChange_(); + }.bind(this)); + }, + + didSelectedNodeChange_() { + this.dispatchEvent(new tr.b.Event('selected-node-changed')); + }, + + get selectedNode() { + return this.selectedNode_; + }, + + set selectedNode(node) { + this.selectedNode_ = node; + this.didSelectedNodeChange_(); + this.scheduleRebuild_(); + }, + + get aggregationMode() { + return this.aggregationMode_; + }, + + set aggregationMode(aggregationMode) { + this.aggregationMode_ = aggregationMode; + this.scheduleRebuild_(); + }, + + onRebuild_() { + if (this.selectedNode_ === undefined) { + this.$.table.clear(); + return; + } + + if (this.$.table.tableRows.includes(this.selectedNode_)) { + this.$.table.selectedTableRow = this.selectedNode_; + return; + } + + this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + this.$.table.userCanModifySortOrder = false; + const rows = this.createRows_(this.selectedNode_); + this.$.table.tableRows = rows; + this.$.table.tableColumns = this.createColumns_(rows); + this.$.table.selectedTableRow = rows[rows.length - 1]; + }, + + createRows_(node) { + const rows = []; + while (node) { + rows.push(node); + node = node.parentNode; + } + rows.reverse(); + return rows; + }, + + createColumns_(rows) { + const titleColumn = new HeapDetailsPathColumn('Current path'); + titleColumn.width = '200px'; + + const numericColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, { + cellKey: 'cells', + aggregationMode: this.aggregationMode_, + rules: tr.ui.analysis.HEAP_DETAILS_COLUMN_RULES, + shouldSetContextGroup: true + }); + tr.ui.analysis.MemoryColumn.spaceEqually(numericColumns); + + return [titleColumn].concat(numericColumns); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_util.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_util.html new file mode 100644 index 00000000000..2cf6c6ec8b1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_util.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const LATIN_SMALL_LETTER_F_WITH_HOOK = String.fromCharCode(0x0192); + const CIRCLED_LATIN_CAPITAL_LETTER_T = String.fromCharCode(0x24C9); + + /** @{enum} */ + const HeapDetailsRowDimension = { + ROOT: {}, + STACK_FRAME: { + label: 'Stack frame', + symbol: LATIN_SMALL_LETTER_F_WITH_HOOK, + color: 'heap_dump_stack_frame' + }, + OBJECT_TYPE: { + label: 'Object type', + symbol: CIRCLED_LATIN_CAPITAL_LETTER_T, + color: 'heap_dump_object_type' + } + }; + + /** @{constructor} */ + function HeapDetailsTitleColumn(title) { + tr.ui.analysis.TitleColumn.call(this, title); + } + + HeapDetailsTitleColumn.prototype = { + __proto__: tr.ui.analysis.TitleColumn.prototype, + + formatTitle(row) { + if (row.dimension === HeapDetailsRowDimension.ROOT) { + return row.title; + } + + const symbolEl = document.createElement('span'); + Polymer.dom(symbolEl).textContent = row.dimension.symbol; + symbolEl.title = row.dimension.label; + symbolEl.style.color = tr.b.ColorScheme.getColorForReservedNameAsString( + row.dimension.color); + symbolEl.style.paddingRight = '4px'; + symbolEl.style.cursor = 'help'; + symbolEl.style.fontWeight = 'bold'; + + const titleEl = document.createElement('span'); + Polymer.dom(titleEl).appendChild(symbolEl); + Polymer.dom(titleEl).appendChild(document.createTextNode(row.title)); + + return titleEl; + } + }; + + /** @constructor */ + function AllocationCountColumn(name, cellPath, aggregationMode) { + tr.ui.analysis.DetailsNumericMemoryColumn.call( + this, name, cellPath, aggregationMode); + } + + AllocationCountColumn.prototype = { + __proto__: tr.ui.analysis.DetailsNumericMemoryColumn.prototype, + + getFormattingContext(unit) { + return { minimumFractionDigits: 0 }; + } + }; + + const HEAP_DETAILS_COLUMN_RULES = [ + { + condition: 'Size', + importance: 2, + columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn + }, + { + condition: 'Count', + importance: 1, + columnConstructor: AllocationCountColumn + }, + { + importance: 0, + columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn + } + ]; + + return { + HeapDetailsRowDimension, + HeapDetailsTitleColumn, + AllocationCountColumn, + HEAP_DETAILS_COLUMN_RULES, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane.html new file mode 100644 index 00000000000..5df80bb88b4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane.html @@ -0,0 +1,774 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/fixed_color_scheme.html"> +<link rel="import" href="/tracing/base/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/unit_scale.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/memory_allocator_dump.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_allocator_details_pane.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html"> +<link rel="import" href="/tracing/ui/analysis/stacked_pane.html"> +<link rel="import" href="/tracing/ui/base/color_legend.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/ui/view_specific_brushing_state.html"> + +<dom-module id='tr-ui-a-memory-dump-overview-pane'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + + #label { + flex: 0 0 auto; + padding: 8px; + + background-color: #eee; + border-bottom: 1px solid #8e8e8e; + border-top: 1px solid white; + + font-size: 15px; + font-weight: bold; + } + + #label a { + font-weight: normal; + float: right; + } + + #contents { + flex: 1 0 auto; + align-self: stretch; + font-size: 12px; + overflow: auto; + } + + #info_text { + padding: 8px; + color: #666; + font-style: italic; + text-align: center; + } + + #table { + display: none; /* Hide until memory dumps are set. */ + flex: 1 0 auto; + align-self: stretch; + font-size: 12px; + } + </style> + <tr-ui-b-view-specific-brushing-state id="state" + view-id="analysis.memory_dump_overview_pane"> + </tr-ui-b-view-specific-brushing-state> + <div id="label">Overview <a href="https://chromium.googlesource.com/chromium/src/+/master/docs/memory-infra">Help</a></div> + <div id="contents"> + <div id="info_text">No memory memory dumps selected</div> + <tr-ui-b-table id="table"></tr-ui-b-table> + </div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const MemoryColumnColorScheme = tr.b.MemoryColumnColorScheme; + const Scalar = tr.b.Scalar; + const sizeInBytes_smallerIsBetter = + tr.b.Unit.byName.sizeInBytes_smallerIsBetter; + + const PLATFORM_SPECIFIC_TOTAL_NAME_SUFFIX = '_bytes'; + + const DISPLAYED_SIZE_NUMERIC_NAME = + tr.model.MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME; + const SOME_TIMESTAMPS_INFO_QUANTIFIER = + tr.ui.analysis.MemoryColumn.SOME_TIMESTAMPS_INFO_QUANTIFIER; + + // Unicode symbols used for memory cell info icons and messages. + const RIGHTWARDS_ARROW_WITH_HOOK = String.fromCharCode(0x21AA); + const RIGHTWARDS_ARROW_FROM_BAR = String.fromCharCode(0x21A6); + const GREATER_THAN_OR_EQUAL_TO = String.fromCharCode(0x2265); + const UNMARRIED_PARTNERSHIP_SYMBOL = String.fromCharCode(0x26AF); + const TRIGRAM_FOR_HEAVEN = String.fromCharCode(0x2630); + + function lazyMap(list, fn, opt_this) { + opt_this = opt_this || this; + let result = undefined; + list.forEach(function(item, index) { + const value = fn.call(opt_this, item, index); + if (value === undefined) return; + if (result === undefined) { + result = new Array(list.length); + } + result[index] = value; + }); + return result; + } + + /** @constructor */ + function ProcessNameColumn() { + tr.ui.analysis.TitleColumn.call(this, 'Process'); + } + + ProcessNameColumn.prototype = { + __proto__: tr.ui.analysis.TitleColumn.prototype, + + formatTitle(row) { + if (row.contexts === undefined) { + return row.title; // Total row. + } + const titleEl = document.createElement('tr-ui-b-color-legend'); + titleEl.label = row.title; + return titleEl; + } + }; + + /** @constructor */ + function UsedMemoryColumn(name, cellPath, aggregationMode) { + tr.ui.analysis.NumericMemoryColumn.call( + this, name, cellPath, aggregationMode); + } + + UsedMemoryColumn.COLOR = + MemoryColumnColorScheme.getColor('used_memory_column').toString(); + UsedMemoryColumn.OLDER_COLOR = + MemoryColumnColorScheme.getColor('older_used_memory_column').toString(); + + UsedMemoryColumn.prototype = { + __proto__: tr.ui.analysis.NumericMemoryColumn.prototype, + + get title() { + return tr.ui.b.createSpan({ + textContent: this.name, + color: UsedMemoryColumn.COLOR + }); + }, + + getFormattingContext(unit) { + return { unitPrefix: tr.b.UnitPrefixScale.BINARY.MEBI }; + }, + + color(numerics, processMemoryDumps) { + return UsedMemoryColumn.COLOR; + }, + + getChildPaneBuilder(processMemoryDumps) { + if (processMemoryDumps === undefined) return undefined; + + const vmRegions = lazyMap(processMemoryDumps, function(pmd) { + if (pmd === undefined) return undefined; + return pmd.mostRecentVmRegions; + }); + if (vmRegions === undefined) return undefined; + + return function() { + const pane = document.createElement( + 'tr-ui-a-memory-dump-vm-regions-details-pane'); + pane.vmRegions = vmRegions; + pane.aggregationMode = this.aggregationMode; + return pane; + }.bind(this); + } + }; + + /** @constructor */ + function PeakMemoryColumn(name, cellPath, aggregationMode) { + UsedMemoryColumn.call(this, name, cellPath, aggregationMode); + } + + PeakMemoryColumn.prototype = { + __proto__: UsedMemoryColumn.prototype, + + addInfos(numerics, processMemoryDumps, infos) { + if (processMemoryDumps === undefined) return; // Total row. + + let resettableValueCount = 0; + let nonResettableValueCount = 0; + for (let i = 0; i < numerics.length; i++) { + if (numerics[i] === undefined) continue; + if (processMemoryDumps[i].arePeakResidentBytesResettable) { + resettableValueCount++; + } else { + nonResettableValueCount++; + } + } + + if (resettableValueCount > 0 && nonResettableValueCount > 0) { + infos.push(tr.ui.analysis.createWarningInfo('Both resettable and ' + + 'non-resettable peak RSS values were provided by the process')); + } else if (resettableValueCount > 0) { + infos.push({ + icon: RIGHTWARDS_ARROW_WITH_HOOK, + message: 'Peak RSS since previous memory dump.' + }); + } else { + infos.push({ + icon: RIGHTWARDS_ARROW_FROM_BAR, + message: 'Peak RSS since process startup. Finer grained ' + + 'peaks require a Linux kernel version ' + + GREATER_THAN_OR_EQUAL_TO + ' 4.0.' + }); + } + } + }; + + /** @constructor */ + function ByteStatColumn(name, cellPath, aggregationMode) { + UsedMemoryColumn.call(this, name, cellPath, aggregationMode); + } + + ByteStatColumn.prototype = { + __proto__: UsedMemoryColumn.prototype, + + color(numerics, processMemoryDumps) { + if (processMemoryDumps === undefined) { + return UsedMemoryColumn.COLOR; // Total row. + } + + const allOlderValues = processMemoryDumps.every( + function(processMemoryDump) { + if (processMemoryDump === undefined) return true; + return !processMemoryDump.hasOwnVmRegions; + }); + + // Show the cell in lighter blue if all values were older (i.e. none of + // the defined process memory dumps had own VM regions). + if (allOlderValues) { + return UsedMemoryColumn.OLDER_COLOR; + } + return UsedMemoryColumn.COLOR; + }, + + addInfos(numerics, processMemoryDumps, infos) { + if (processMemoryDumps === undefined) return; // Total row. + + let olderValueCount = 0; + for (let i = 0; i < numerics.length; i++) { + const processMemoryDump = processMemoryDumps[i]; + if (processMemoryDump !== undefined && + !processMemoryDump.hasOwnVmRegions) { + olderValueCount++; + } + } + + if (olderValueCount === 0) { + return; // There are no older values. + } + + const infoQuantifier = olderValueCount < numerics.length ? + ' ' + SOME_TIMESTAMPS_INFO_QUANTIFIER : /* some values are older */ + ''; /* all values are older */ + + // Emit an info if there was at least one older value (i.e. at least one + // defined process memory dump did not have own VM regions). + infos.push({ + message: 'Older value' + infoQuantifier + + ' (only heavy (purple) memory dumps contain memory maps).', + icon: UNMARRIED_PARTNERSHIP_SYMBOL + }); + } + }; + + // Rules for constructing and sorting used memory columns. + UsedMemoryColumn.RULES = [ + { + condition: 'Total resident', + importance: 10, + columnConstructor: UsedMemoryColumn + }, + { + condition: 'Peak total resident', + importance: 9, + columnConstructor: PeakMemoryColumn + }, + { + condition: 'PSS', + importance: 8, + columnConstructor: ByteStatColumn + }, + { + condition: 'Private dirty', + importance: 7, + columnConstructor: ByteStatColumn + }, + { + condition: 'Swapped', + importance: 6, + columnConstructor: ByteStatColumn + }, + { + // All other columns. + importance: 0, + columnConstructor: UsedMemoryColumn + } + ]; + + // Map from ProcessMemoryDump totals fields to column names. + UsedMemoryColumn.TOTALS_MAP = { + 'residentBytes': 'Total resident', + 'peakResidentBytes': 'Peak total resident', + 'privateFootprintBytes': 'Private footprint', + }; + + // Map from ProcessMemoryDump platform-specific totals fields to column names. + UsedMemoryColumn.PLATFORM_SPECIFIC_TOTALS_MAP = { + 'vm': 'Total virtual', + 'swp': 'Swapped', + 'pc': 'Private clean', + 'pd': 'Private dirty', + 'sc': 'Shared clean', + 'sd': 'Shared dirty', + 'gpu_egl': 'GPU EGL', + 'gpu_egl_pss': 'GPU EGL PSS', + 'gpu_gl': 'GPU GL', + 'gpu_gl_pss': 'GPU GL PSS', + 'gpu_etc': 'GPU Other', + 'gpu_etc_pss': 'GPU Other PSS', + }; + + // Map from VMRegionByteStats field names to column names. + UsedMemoryColumn.BYTE_STAT_MAP = { + 'proportionalResident': 'PSS', + 'privateDirtyResident': 'Private dirty', + 'swapped': 'Swapped' + }; + + /** @constructor */ + function AllocatorColumn(name, cellPath, aggregationMode) { + tr.ui.analysis.NumericMemoryColumn.call( + this, name, cellPath, aggregationMode); + } + + AllocatorColumn.prototype = { + __proto__: tr.ui.analysis.NumericMemoryColumn.prototype, + + get title() { + const titleEl = document.createElement('tr-ui-b-color-legend'); + titleEl.label = this.name; + return titleEl; + }, + + getFormattingContext(unit) { + return { unitPrefix: tr.b.UnitPrefixScale.BINARY.MEBI }; + }, + + addInfos(numerics, processMemoryDumps, infos) { + if (processMemoryDumps === undefined) return; + + let heapDumpCount = 0; + let missingSizeCount = 0; + + for (let i = 0; i < processMemoryDumps.length; i++) { + const processMemoryDump = processMemoryDumps[i]; + if (processMemoryDump === undefined) continue; + + const heapDumps = processMemoryDump.heapDumps; + if (heapDumps !== undefined && heapDumps[this.name] !== undefined) { + heapDumpCount++; + } + const allocatorDump = + processMemoryDump.getMemoryAllocatorDumpByFullName(this.name); + + if (allocatorDump !== undefined && + allocatorDump.numerics[DISPLAYED_SIZE_NUMERIC_NAME] === undefined) { + missingSizeCount++; + } + } + + // Emit a heap dump info if at least one of the process memory dumps has + // a heap dump associated with this allocator. + if (heapDumpCount > 0) { + const infoQuantifier = heapDumpCount < numerics.length ? + ' ' + SOME_TIMESTAMPS_INFO_QUANTIFIER : ''; + infos.push({ + message: 'Heap dump provided' + infoQuantifier + '.', + icon: TRIGRAM_FOR_HEAVEN + }); + } + + // Emit a warning if this allocator did not provide size in at least one + // of the process memory dumps. + if (missingSizeCount > 0) { + const infoQuantifier = missingSizeCount < numerics.length ? + ' ' + SOME_TIMESTAMPS_INFO_QUANTIFIER : ''; + infos.push(tr.ui.analysis.createWarningInfo( + 'Size was not provided' + infoQuantifier + '.')); + } + }, + + getChildPaneBuilder(processMemoryDumps) { + if (processMemoryDumps === undefined) return undefined; + + const memoryAllocatorDumps = lazyMap(processMemoryDumps, function(pmd) { + if (pmd === undefined) return undefined; + return pmd.getMemoryAllocatorDumpByFullName(this.name); + }, this); + if (memoryAllocatorDumps === undefined) return undefined; + + const heapDumps = lazyMap(processMemoryDumps, function(pmd) { + if (pmd === undefined || pmd.heapDumps === undefined) return undefined; + return pmd.heapDumps[this.name]; + }, this); + + return function() { + const pane = document.createElement( + 'tr-ui-a-memory-dump-allocator-details-pane'); + pane.memoryAllocatorDumps = memoryAllocatorDumps; + pane.heapDumps = heapDumps; + pane.aggregationMode = this.aggregationMode; + return pane; + }.bind(this); + } + }; + + /** @constructor */ + function TracingColumn(name, cellPath, aggregationMode) { + AllocatorColumn.call(this, name, cellPath, aggregationMode); + } + + TracingColumn.COLOR = + MemoryColumnColorScheme.getColor('tracing_memory_column').toString(); + + TracingColumn.prototype = { + __proto__: AllocatorColumn.prototype, + + get title() { + return tr.ui.b.createSpan({ + textContent: this.name, + color: TracingColumn.COLOR + }); + }, + + color(numerics, processMemoryDumps) { + return TracingColumn.COLOR; + } + }; + + // Rules for constructing and sorting allocator columns. + AllocatorColumn.RULES = [ + { + condition: 'tracing', + importance: 0, + columnConstructor: TracingColumn + }, + { + // All other columns. + importance: 1, + columnConstructor: AllocatorColumn + } + ]; + + Polymer({ + is: 'tr-ui-a-memory-dump-overview-pane', + behaviors: [tr.ui.analysis.StackedPane], + + created() { + this.processMemoryDumps_ = undefined; + this.aggregationMode_ = undefined; + }, + + ready() { + this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.CELL; + this.$.table.addEventListener('selection-changed', + function(tableEvent) { + tableEvent.stopPropagation(); + this.changeChildPane_(); + }.bind(this)); + }, + + /** + * Sets the process memory dumps and schedules rebuilding the pane. + * + * The provided value should be a chronological list of dictionaries + * mapping process IDs to process memory dumps. Example: + * + * [ + * { + * // PMDs at timestamp 1. + * 42: tr.model.ProcessMemoryDump {} + * }, + * { + * // PMDs at timestamp 2. + * 42: tr.model.ProcessMemoryDump {}, + * 89: tr.model.ProcessMemoryDump {} + * } + * ] + */ + set processMemoryDumps(processMemoryDumps) { + this.processMemoryDumps_ = processMemoryDumps; + this.scheduleRebuild_(); + }, + + get processMemoryDumps() { + return this.processMemoryDumps_; + }, + + set aggregationMode(aggregationMode) { + this.aggregationMode_ = aggregationMode; + this.scheduleRebuild_(); + }, + + get aggregationMode() { + return this.aggregationMode_; + }, + + get selectedMemoryCell() { + if (this.processMemoryDumps_ === undefined || + this.processMemoryDumps_.length === 0) { + return undefined; + } + + const selectedTableRow = this.$.table.selectedTableRow; + if (!selectedTableRow) return undefined; + + const selectedColumnIndex = this.$.table.selectedColumnIndex; + if (selectedColumnIndex === undefined) return undefined; + + const selectedColumn = this.$.table.tableColumns[selectedColumnIndex]; + const selectedMemoryCell = selectedColumn.cell(selectedTableRow); + return selectedMemoryCell; + }, + + changeChildPane_() { + this.storeSelection_(); + this.childPaneBuilder = this.determineChildPaneBuilderFromSelection_(); + }, + + determineChildPaneBuilderFromSelection_() { + if (this.processMemoryDumps_ === undefined || + this.processMemoryDumps_.length === 0) { + return undefined; + } + + const selectedTableRow = this.$.table.selectedTableRow; + if (!selectedTableRow) return undefined; + + const selectedColumnIndex = this.$.table.selectedColumnIndex; + if (selectedColumnIndex === undefined) return undefined; + const selectedColumn = this.$.table.tableColumns[selectedColumnIndex]; + + return selectedColumn.getChildPaneBuilder(selectedTableRow.contexts); + }, + + onRebuild_() { + if (this.processMemoryDumps_ === undefined || + this.processMemoryDumps_.length === 0) { + // Show the info text (hide the table). + this.$.info_text.style.display = 'block'; + this.$.table.style.display = 'none'; + + this.$.table.clear(); + this.$.table.rebuild(); + return; + } + + // Show the table (hide the info text). + this.$.info_text.style.display = 'none'; + this.$.table.style.display = 'block'; + + const rows = this.createRows_(); + const columns = this.createColumns_(rows); + const footerRows = this.createFooterRows_(rows, columns); + + this.$.table.tableRows = rows; + this.$.table.footerRows = footerRows; + this.$.table.tableColumns = columns; + this.$.table.rebuild(); + + this.restoreSelection_(); + }, + + createRows_() { + // Timestamp (list index) -> Process ID (dict key) -> PMD. + const timeToPidToProcessMemoryDump = this.processMemoryDumps_; + + // Process ID (dict key) -> Timestamp (list index) -> PMD or undefined. + const pidToTimeToProcessMemoryDump = tr.b.invertArrayOfDicts( + timeToPidToProcessMemoryDump); + + // Process (list index) -> Component (dict key) -> Cell. + const rows = []; + for (const [pid, timeToDump] of + Object.entries(pidToTimeToProcessMemoryDump)) { + // Get the process associated with the dumps. We can use any defined + // process memory dump in timeToDump since they all have the same + // pid. + const process = timeToDump.find(x => x).process; + + // Used memory (total resident, PSS, ...). + const usedMemoryCells = tr.ui.analysis.createCells(timeToDump, + function(dump) { + const sizes = {}; + + const totals = dump.totals; + if (totals !== undefined) { + // Common totals. + for (const [totalName, cellName] of + Object.entries(UsedMemoryColumn.TOTALS_MAP)) { + const total = totals[totalName]; + if (total === undefined) continue; + sizes[cellName] = new Scalar( + sizeInBytes_smallerIsBetter, total); + } + + // Platform-specific totals (e.g. private resident on Mac). + const platformSpecific = totals.platformSpecific; + if (platformSpecific !== undefined) { + for (const [name, size] of Object.entries(platformSpecific)) { + let newName = name; + if (UsedMemoryColumn.PLATFORM_SPECIFIC_TOTALS_MAP[name] === + undefined) { + // Change raw OS-specific total name to a friendly + // column title (e.g. 'private_bytes' -> 'Private'). + if (name.endsWith(PLATFORM_SPECIFIC_TOTAL_NAME_SUFFIX)) { + newName = name.substring(0, name.length - + PLATFORM_SPECIFIC_TOTAL_NAME_SUFFIX.length); + } + newName = newName.replace('_', ' ').trim(); + newName = + newName.charAt(0).toUpperCase() + newName.slice(1); + } else { + newName = + UsedMemoryColumn.PLATFORM_SPECIFIC_TOTALS_MAP[name]; + } + sizes[newName] = new Scalar( + sizeInBytes_smallerIsBetter, size); + } + } + } + + // VM regions byte stats. + const vmRegions = dump.mostRecentVmRegions; + if (vmRegions !== undefined) { + for (const [byteStatName, cellName] of + Object.entries(UsedMemoryColumn.BYTE_STAT_MAP)) { + const byteStat = vmRegions.byteStats[byteStatName]; + if (byteStat === undefined) continue; + sizes[cellName] = new Scalar( + sizeInBytes_smallerIsBetter, byteStat); + } + } + + return sizes; + }); + + // Allocator memory (v8, oilpan, ...). + const allocatorCells = tr.ui.analysis.createCells(timeToDump, + function(dump) { + const memoryAllocatorDumps = dump.memoryAllocatorDumps; + if (memoryAllocatorDumps === undefined) return undefined; + + const sizes = {}; + memoryAllocatorDumps.forEach(function(allocatorDump) { + let rootDisplayedSizeNumeric = allocatorDump.numerics[ + DISPLAYED_SIZE_NUMERIC_NAME]; + if (rootDisplayedSizeNumeric === undefined) { + rootDisplayedSizeNumeric = + new Scalar(sizeInBytes_smallerIsBetter, 0); + } + sizes[allocatorDump.fullName] = rootDisplayedSizeNumeric; + }); + return sizes; + }); + + rows.push({ + title: process.userFriendlyName, + contexts: timeToDump, + usedMemoryCells, + allocatorCells + }); + } + return rows; + }, + + createFooterRows_(rows, columns) { + // Add a 'Total' row if there are at least two process memory dumps. + if (rows.length <= 1) return []; + + const totalRow = {title: 'Total'}; + tr.ui.analysis.aggregateTableRowCells(totalRow, rows, columns); + + return [totalRow]; + }, + + createColumns_(rows) { + const titleColumn = new ProcessNameColumn(); + titleColumn.width = '200px'; + + const usedMemorySizeColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, { + cellKey: 'usedMemoryCells', + aggregationMode: this.aggregationMode_, + rules: UsedMemoryColumn.RULES + }); + + const allocatorSizeColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, { + cellKey: 'allocatorCells', + aggregationMode: this.aggregationMode_, + rules: AllocatorColumn.RULES + }); + + const sizeColumns = usedMemorySizeColumns.concat(allocatorSizeColumns); + tr.ui.analysis.MemoryColumn.spaceEqually(sizeColumns); + + const columns = [titleColumn].concat(sizeColumns); + return columns; + }, + + storeSelection_() { + let selectedRowTitle; + const selectedRow = this.$.table.selectedTableRow; + if (selectedRow !== undefined) { + selectedRowTitle = selectedRow.title; + } + + let selectedColumnName; + const selectedColumnIndex = this.$.table.selectedColumnIndex; + if (selectedColumnIndex !== undefined) { + const selectedColumn = this.$.table.tableColumns[selectedColumnIndex]; + selectedColumnName = selectedColumn.name; + } + + this.$.state.set( + {rowTitle: selectedRowTitle, columnName: selectedColumnName}); + }, + + restoreSelection_() { + const settings = this.$.state.get(); + if (settings === undefined || settings.rowTitle === undefined || + settings.columnName === undefined) { + return; + } + + const selectedColumnIndex = this.$.table.tableColumns.findIndex( + col => col.name === settings.columnName); + if (selectedColumnIndex === -1) return; + + const selectedRowTitle = settings.rowTitle; + const selectedRow = this.$.table.tableRows.find( + row => row.title === selectedRowTitle); + if (selectedRow === undefined) return; + + this.$.table.selectedTableRow = selectedRow; + this.$.table.selectedColumnIndex = selectedColumnIndex; + } + }); + + return { + // All exports are for testing only. + ProcessNameColumn, + UsedMemoryColumn, + PeakMemoryColumn, + ByteStatColumn, + AllocatorColumn, + TracingColumn, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane_test.html new file mode 100644 index 00000000000..80127e020a8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane_test.html @@ -0,0 +1,840 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/heap_dump.html"> +<link rel="import" href="/tracing/model/memory_allocator_dump.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_overview_pane.html"> +<link rel="import" + href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> +<link rel="import" href="/tracing/ui/brushing_state_controller.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Scalar = tr.b.Scalar; + const sizeInBytes_smallerIsBetter = + tr.b.Unit.byName.sizeInBytes_smallerIsBetter; + const MemoryAllocatorDump = tr.model.MemoryAllocatorDump; + const HeapDump = tr.model.HeapDump; + const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode; + const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields; + const checkColor = tr.ui.analysis.checkColor; + const checkColumns = tr.ui.analysis.checkColumns; + const checkColumnInfosAndColor = tr.ui.analysis.checkColumnInfosAndColor; + const convertToProcessMemoryDumps = + tr.ui.analysis.convertToProcessMemoryDumps; + const extractProcessMemoryDumps = tr.ui.analysis.extractProcessMemoryDumps; + const extractVmRegions = tr.ui.analysis.extractVmRegions; + const extractMemoryAllocatorDumps = + tr.ui.analysis.extractMemoryAllocatorDumps; + const isElementDisplayed = tr.ui.analysis.isElementDisplayed; + const addProcessMemoryDump = + tr.model.MemoryDumpTestUtils.addProcessMemoryDump; + const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump; + const ProcessNameColumn = tr.ui.analysis.ProcessNameColumn; + const UsedMemoryColumn = tr.ui.analysis.UsedMemoryColumn; + const PeakMemoryColumn = tr.ui.analysis.PeakMemoryColumn; + const ByteStatColumn = tr.ui.analysis.ByteStatColumn; + const AllocatorColumn = tr.ui.analysis.AllocatorColumn; + const TracingColumn = tr.ui.analysis.TracingColumn; + + function spanMatcher(expectedTitle) { + return function(actualTitle) { + assert.instanceOf(actualTitle, HTMLElement); + assert.strictEqual(actualTitle.tagName, 'SPAN'); + assert.strictEqual(Polymer.dom(actualTitle).textContent, expectedTitle); + }; + } + + function colorLegendMatcher(expectedTitle) { + return function(actualTitle) { + assert.instanceOf(actualTitle, HTMLElement); + assert.strictEqual(actualTitle.tagName, 'TR-UI-B-COLOR-LEGEND'); + assert.strictEqual(actualTitle.label, expectedTitle); + }; + } + + const EXPECTED_COLUMNS = [ + { title: 'Process', type: ProcessNameColumn, noAggregation: true }, + { title: spanMatcher('Total resident'), type: UsedMemoryColumn }, + { title: spanMatcher('Peak total resident'), type: PeakMemoryColumn }, + { title: spanMatcher('PSS'), type: ByteStatColumn }, + { title: spanMatcher('Private dirty'), type: ByteStatColumn }, + { title: spanMatcher('Swapped'), type: ByteStatColumn }, + { title: spanMatcher('Private'), type: UsedMemoryColumn }, + { title: spanMatcher('Private footprint'), type: UsedMemoryColumn }, + { title: colorLegendMatcher('blink'), type: AllocatorColumn }, + { title: colorLegendMatcher('gpu'), type: AllocatorColumn }, + { title: colorLegendMatcher('malloc'), type: AllocatorColumn }, + { title: colorLegendMatcher('oilpan'), type: AllocatorColumn }, + { title: colorLegendMatcher('v8'), type: AllocatorColumn }, + { title: spanMatcher('tracing'), type: TracingColumn } + ]; + + function checkRow(columns, row, expectedTitle, expectedSizes, + expectedContexts) { + // Check title. + const formattedTitle = columns[0].formatTitle(row); + if (typeof expectedTitle === 'function') { + expectedTitle(formattedTitle); + } else { + assert.strictEqual(formattedTitle, expectedTitle); + } + + // Check all sizes. The first assert below is a test sanity check. + assert.lengthOf(expectedSizes, columns.length - 1 /* all except title */); + for (let i = 0; i < expectedSizes.length; i++) { + checkSizeNumericFields(row, columns[i + 1], expectedSizes[i]); + } + + // There should be no row nesting on the overview pane. + assert.isUndefined(row.subRows); + + if (expectedContexts) { + assert.deepEqual(Array.from(row.contexts), expectedContexts); + } else { + assert.isUndefined(row.contexts); + } + } + + function checkRows(columns, actualRows, expectedRows) { + if (expectedRows === undefined) { + assert.isUndefined(actualRows); + return; + } + assert.lengthOf(actualRows, expectedRows.length); + for (let i = 0; i < expectedRows.length; i++) { + const actualRow = actualRows[i]; + const expectedRow = expectedRows[i]; + checkRow(columns, actualRow, expectedRow.title, expectedRow.sizes, + expectedRow.contexts); + } + } + + function checkSpanWithColor(span, expectedText, expectedColor) { + assert.strictEqual(span.tagName, 'SPAN'); + assert.strictEqual(Polymer.dom(span).textContent, expectedText); + checkColor(span.style.color, expectedColor); + } + + function checkColorLegend(legend, expectedLabel) { + assert.strictEqual(legend.tagName, 'TR-UI-B-COLOR-LEGEND'); + assert.strictEqual(legend.label, expectedLabel); + } + + function createAndCheckMemoryDumpOverviewPane( + test, processMemoryDumps, expectedRows, expectedFooterRows, + aggregationMode) { + const viewEl = + tr.ui.analysis.createTestPane('tr-ui-a-memory-dump-overview-pane'); + viewEl.processMemoryDumps = processMemoryDumps; + viewEl.aggregationMode = aggregationMode; + viewEl.rebuild(); + test.addHTMLOutput(viewEl); + + // Check that the table is shown. + assert.isTrue(isElementDisplayed(viewEl.$.table)); + assert.isFalse(isElementDisplayed(viewEl.$.info_text)); + + assert.isUndefined(viewEl.createChildPane()); + + const table = viewEl.$.table; + const columns = table.tableColumns; + checkColumns(columns, EXPECTED_COLUMNS, aggregationMode); + const rows = table.tableRows; + + checkRows(columns, table.tableRows, expectedRows); + checkRows(columns, table.footerRows, expectedFooterRows); + } + + const FIELD = 1 << 0; + const DUMP = 1 << 1; + + function checkOverviewColumnInfosAndColor(column, fieldAndDumpMask, + dumpCreatedCallback, expectedInfos, expectedColorReservedName) { + const fields = fieldAndDumpMask.map(function(mask, index) { + return mask & FIELD ? + new Scalar(sizeInBytes_smallerIsBetter, 1024 + 32 * index) : + undefined; + }); + + let contexts; + if (dumpCreatedCallback === undefined) { + contexts = undefined; + } else { + tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + fieldAndDumpMask.forEach(function(mask, i) { + const timestamp = 10 + i; + const gmd = addGlobalMemoryDump(model, {ts: timestamp}); + if (mask & DUMP) { + const pmd = addProcessMemoryDump(gmd, process, {ts: timestamp}); + dumpCreatedCallback(pmd, mask); + } + }); + contexts = model.globalMemoryDumps.map(function(gmd) { + return gmd.processMemoryDumps[1]; + }); + }); + } + + checkColumnInfosAndColor( + column, fields, contexts, expectedInfos, expectedColorReservedName); + } + + test('colorsAreDefined', function() { + // We use these constants in the code and the tests so here we guard + // against them being undefined and causing all the tests to still + // pass while the we end up with no colors. + assert.isDefined(UsedMemoryColumn.COLOR); + assert.isDefined(UsedMemoryColumn.OLDER_COLOR); + assert.isDefined(TracingColumn.COLOR); + }); + + test('instantiate_empty', function() { + tr.ui.analysis.createAndCheckEmptyPanes(this, + 'tr-ui-a-memory-dump-overview-pane', 'processMemoryDumps', + function(viewEl) { + // Check that the info text is shown. + assert.isTrue(isElementDisplayed(viewEl.$.info_text)); + assert.isFalse(isElementDisplayed(viewEl.$.table)); + }); + }); + + test('instantiate_singleGlobalMemoryDump', function() { + const processMemoryDumps = convertToProcessMemoryDumps( + [tr.ui.analysis.createSingleTestGlobalMemoryDump()]); + createAndCheckMemoryDumpOverviewPane(this, + processMemoryDumps, + [ // Table rows. + { + title: colorLegendMatcher('Process 1'), + sizes: [[29884416], undefined, [9437184], [5767168], undefined, + undefined, undefined, undefined, undefined, [7340032], undefined, + undefined, [2097152]], + contexts: extractProcessMemoryDumps(processMemoryDumps, 1) + }, + { + title: colorLegendMatcher('Process 2'), + sizes: [[17825792], [39845888], [18350080], [0], [32], [8912896], + [15728640], [7340032], [0], [1048576], [1], [5242880], + [1572864]], + contexts: extractProcessMemoryDumps(processMemoryDumps, 2) + }, + { + title: colorLegendMatcher('Process 4'), + sizes: [undefined, [17825792], undefined, undefined, undefined, + undefined, undefined, undefined, undefined, undefined, + undefined, undefined, undefined], + contexts: extractProcessMemoryDumps(processMemoryDumps, 4) + } + ], + [ // Footer rows. + { + title: 'Total', + sizes: [[47710208], [57671680], [27787264], [5767168], [32], + [8912896], [15728640], [7340032], [0], [8388608], [1], + [5242880], [3670016]], + contexts: undefined + } + ], + undefined /* no aggregation */); + }); + + test('instantiate_multipleGlobalMemoryDumps', function() { + const processMemoryDumps = convertToProcessMemoryDumps( + tr.ui.analysis.createMultipleTestGlobalMemoryDumps()); + createAndCheckMemoryDumpOverviewPane(this, + processMemoryDumps, + [ // Table rows. + { + title: colorLegendMatcher('Process 1'), + sizes: [[31457280, 29884416, undefined], undefined, + [10485760, 9437184, undefined], [8388608, 5767168, undefined], + undefined, undefined, undefined, undefined, undefined, + [undefined, 7340032, undefined], undefined, undefined, + [undefined, 2097152, undefined]], + contexts: extractProcessMemoryDumps(processMemoryDumps, 1) + }, + { + title: colorLegendMatcher('Process 2'), + sizes: [[19398656, 17825792, 15728640], + [40370176, 39845888, 40894464], [18350080, 18350080, 18350080], + [0, 0, -2621440], [32, 32, 64], [10485760, 8912896, 7340032], + [15728640, 15728640, 15728640], [undefined, 7340032, 6291456], + [undefined, 0, 1048576], [2097152, 1048576, 786432], + [undefined, 1, undefined], [5242880, 5242880, 5767168], + [1048576, 1572864, 2097152]], + contexts: extractProcessMemoryDumps(processMemoryDumps, 2) + }, + { + title: colorLegendMatcher('Process 3'), + sizes: [undefined, undefined, undefined, undefined, undefined, + undefined, undefined, undefined, undefined, undefined, + [2147483648, undefined, 1073741824], + [1073741824, undefined, 2147483648], undefined], + contexts: extractProcessMemoryDumps(processMemoryDumps, 3) + }, + { + title: colorLegendMatcher('Process 4'), + sizes: [undefined, [undefined, 17825792, 17825792], undefined, + undefined, undefined, undefined, undefined, undefined, + undefined, undefined, undefined, undefined, undefined], + contexts: extractProcessMemoryDumps(processMemoryDumps, 4) + } + ], + [ // Footer rows. + { + title: 'Total', + sizes: [[50855936, 47710208, 15728640], + [40370176, 57671680, 58720256], [28835840, 27787264, 18350080], + [8388608, 5767168, -2621440], [32, 32, 64], + [10485760, 8912896, 7340032], [15728640, 15728640, 15728640], + [undefined, 7340032, 6291456], [undefined, 0, 1048576], + [2097152, 8388608, 786432], [2147483648, 1, 1073741824], + [1078984704, 5242880, 2153250816], + [1048576, 3670016, 2097152]], + contexts: undefined + } + ], + AggregationMode.DIFF); + }); + + test('instantiate_singleProcessMemoryDump', function() { + const processMemoryDumps = convertToProcessMemoryDumps( + [tr.ui.analysis.createSingleTestProcessMemoryDump()]); + createAndCheckMemoryDumpOverviewPane(this, + processMemoryDumps, + [ // Table rows. + { + title: colorLegendMatcher('Process 2'), + sizes: [[17825792], [39845888], [18350080], [0], [32], [8912896], + [15728640], [7340032], [0], [1048576], [1], [5242880], + [1572864]], + contexts: extractProcessMemoryDumps(processMemoryDumps, 2) + } + ], + [] /* footer rows */, + undefined /* no aggregation */); + }); + + test('instantiate_multipleProcessMemoryDumps', function() { + const processMemoryDumps = convertToProcessMemoryDumps( + tr.ui.analysis.createMultipleTestProcessMemoryDumps()); + createAndCheckMemoryDumpOverviewPane(this, + processMemoryDumps, + [ // Table rows. + { + title: colorLegendMatcher('Process 2'), + sizes: [[19398656, 17825792, 15728640], + [40370176, 39845888, 40894464], [18350080, 18350080, 18350080], + [0, 0, -2621440], [32, 32, 64], [10485760, 8912896, 7340032], + [15728640, 15728640, 15728640], [undefined, 7340032, 6291456], + [undefined, 0, 1048576], [2097152, 1048576, 786432], + [undefined, 1, undefined], [5242880, 5242880, 5767168], + [1048576, 1572864, 2097152]], + contexts: extractProcessMemoryDumps(processMemoryDumps, 2) + } + ], + [] /* footer rows */, + AggregationMode.MAX); + }); + + test('selection', function() { + const processMemoryDumps = convertToProcessMemoryDumps( + tr.ui.analysis.createMultipleTestGlobalMemoryDumps()); + + const viewEl = + tr.ui.analysis.createTestPane('tr-ui-a-memory-dump-overview-pane'); + viewEl.processMemoryDumps = processMemoryDumps; + viewEl.aggregationMode = AggregationMode.DIFF; + viewEl.rebuild(); + this.addHTMLOutput(viewEl); + + const table = viewEl.$.table; + + // Simulate clicking on the 'malloc' cell of the second process. + table.selectedTableRow = table.tableRows[1]; + table.selectedColumnIndex = 10; + assert.lengthOf(viewEl.requestedChildPanes, 2); + let lastChildPane = viewEl.requestedChildPanes[1]; + assert.strictEqual( + lastChildPane.tagName, 'TR-UI-A-MEMORY-DUMP-ALLOCATOR-DETAILS-PANE'); + assert.strictEqual(lastChildPane.aggregationMode, AggregationMode.DIFF); + assert.deepEqual(lastChildPane.memoryAllocatorDumps, + extractMemoryAllocatorDumps(processMemoryDumps, 2, 'malloc')); + + // Simulate clicking on the 'Oilpan' cell of the second process. + table.selectedColumnIndex = 10; + assert.lengthOf(viewEl.requestedChildPanes, 3); + lastChildPane = viewEl.requestedChildPanes[2]; + assert.isUndefined(viewEl.lastChildPane); + }); + + test('memory', function() { + const processMemoryDumps = convertToProcessMemoryDumps( + tr.ui.analysis.createMultipleTestGlobalMemoryDumps()); + const containerEl = document.createElement('div'); + containerEl.brushingStateController = + new tr.c.BrushingStateController(undefined); + + function simulateView(pids, aggregationMode, + expectedSelectedCellFieldValues, expectedSelectedRowTitle, + expectedSelectedColumnIndex, callback) { + const viewEl = + tr.ui.analysis.createTestPane('tr-ui-a-memory-dump-overview-pane'); + const table = viewEl.$.table; + Polymer.dom(containerEl).textContent = ''; + Polymer.dom(containerEl).appendChild(viewEl); + + const displayedProcessMemoryDumps = processMemoryDumps.map( + function(memoryDumps) { + const result = {}; + for (const [pid, pmd] of Object.entries(memoryDumps)) { + if (pids.includes(pmd.process.pid)) result[pid] = pmd; + } + return result; + }); + viewEl.processMemoryDumps = displayedProcessMemoryDumps; + viewEl.aggregationMode = aggregationMode; + viewEl.rebuild(); + + if (expectedSelectedCellFieldValues === undefined) { + assert.isUndefined(viewEl.childPaneBuilder); + } else { + checkSizeNumericFields(table.selectedTableRow, + table.tableColumns[table.selectedColumnIndex], + expectedSelectedCellFieldValues); + } + + assert.strictEqual( + table.selectedColumnIndex, expectedSelectedColumnIndex); + if (expectedSelectedRowTitle === undefined) { + assert.isUndefined(table.selectedTableRow); + } else { + assert.strictEqual( + table.selectedTableRow.title, expectedSelectedRowTitle); + } + + callback(viewEl, viewEl.$.table); + } + + simulateView( + [1, 2, 3, 4], // All processes. + AggregationMode.DIFF, + undefined, undefined, undefined, // No cell should be selected. + function(view, table) { + assert.isUndefined(view.createChildPane()); + + // Select the 'PSS' column of the second process. + table.selectedTableRow = table.tableRows[1]; + table.selectedColumnIndex = 3; + }); + + simulateView( + [2, 3], + AggregationMode.MAX, + [18350080, 18350080, 18350080], 'Process 2', 3, /* PSS */ + function(view, table) { + const childPane = view.createChildPane(); + assert.strictEqual( + childPane.tagName, 'TR-UI-A-MEMORY-DUMP-VM-REGIONS-DETAILS-PANE'); + assert.deepEqual(Array.from(childPane.vmRegions), + extractVmRegions(processMemoryDumps, 2)); + assert.strictEqual(childPane.aggregationMode, AggregationMode.MAX); + }); + + simulateView( + [3], + undefined, /* No aggregation */ + undefined, undefined, undefined, // No cell selected. + function(view, table) { + assert.isUndefined(view.createChildPane()); + }); + + simulateView( + [1, 2, 3, 4], + AggregationMode.DIFF, + [18350080, 18350080, 18350080], 'Process 2', 3, /* PSS */ + function(view, table) { + const childPane = view.createChildPane(); + assert.strictEqual( + childPane.tagName, 'TR-UI-A-MEMORY-DUMP-VM-REGIONS-DETAILS-PANE'); + assert.deepEqual(Array.from(childPane.vmRegions), + extractVmRegions(processMemoryDumps, 2)); + assert.strictEqual(childPane.aggregationMode, AggregationMode.DIFF); + + // Select the 'v8' column of the first process (empty cell). + table.selectedTableRow = table.tableRows[0]; + table.selectedColumnIndex = 11; + }); + + simulateView( + [1], + undefined, /* No aggregation */ + undefined, undefined, undefined, // No cell should selected. + function(view, table) { + assert.isUndefined(view.createChildPane()); + + // Select 'Total resident' column of the first process. + table.selectedTableRow = table.tableRows[0]; + table.selectedColumnIndex = 1; + }); + + simulateView( + [1, 2, 3, 4], + AggregationMode.MAX, + [31457280, 29884416, undefined], 'Process 1', 1, /* Total resident */ + function(view, table) { + const childPane = view.createChildPane(); + assert.strictEqual( + childPane.tagName, 'TR-UI-A-MEMORY-DUMP-VM-REGIONS-DETAILS-PANE'); + assert.deepEqual(Array.from(childPane.vmRegions), + extractVmRegions(processMemoryDumps, 1)); + assert.strictEqual(childPane.aggregationMode, AggregationMode.MAX); + }); + }); + + test('processNameColumn_formatTitle', function() { + const c = new ProcessNameColumn(); + + // With context (total row). + assert.strictEqual(c.formatTitle({ + title: 'Total', + usedMemoryCells: {} + }), 'Total'); + + // Without context (process row). + const title = c.formatTitle({ + title: 'Process 1', + usedMemoryCells: {}, + contexts: [tr.ui.analysis.createSingleTestProcessMemoryDump()] + }); + checkColorLegend(title, 'Process 1'); + }); + + test('usedMemoryColumn', function() { + const c = new UsedMemoryColumn('Private', 'bytes', (x => x), + AggregationMode.DIFF); + checkSpanWithColor(c.title, 'Private', + UsedMemoryColumn.COLOR /* blue (column title) */); + checkColor(c.color(undefined /* contexts */), + UsedMemoryColumn.COLOR /* blue (column cells) */); + }); + + test('peakMemoryColumn', function() { + const c = new PeakMemoryColumn('Peak', 'bytes', (x => x), + AggregationMode.MAX); + checkSpanWithColor(c.title, 'Peak', + UsedMemoryColumn.COLOR /* blue (column title) */); + checkColor(c.color(undefined) /* contexts */, + UsedMemoryColumn.COLOR /* blue (column cells) */); + + const RESETTABLE_PEAK = 1 << 2; + const NON_RESETTABLE_PEAK = 1 << 3; + function checkPeakColumnInfosAndColor(fieldAndDumpMask, expectedInfos) { + checkOverviewColumnInfosAndColor(c, + fieldAndDumpMask, + function(pmd, mask) { + if (mask & RESETTABLE_PEAK) { + assert.strictEqual( + mask & NON_RESETTABLE_PEAK, 0); // Test sanity check. + pmd.arePeakResidentBytesResettable = true; + } else if (mask & NON_RESETTABLE_PEAK) { + pmd.arePeakResidentBytesResettable = false; + } + }, + expectedInfos, + UsedMemoryColumn.COLOR); + } + + // No context. + checkOverviewColumnInfosAndColor(c, + [FIELD], + undefined /* no context */, + [] /* no infos */, + UsedMemoryColumn.COLOR /* blue color */); + checkOverviewColumnInfosAndColor(c, + [FIELD, FIELD, 0, FIELD], + undefined /* no context */, + [] /* no infos */, + UsedMemoryColumn.COLOR /* blue color */); + + // All resettable. + const EXPECTED_RESETTABLE_INFO = { + icon: '\u21AA', + message: 'Peak RSS since previous memory dump.' + }; + checkPeakColumnInfosAndColor([ + FIELD | DUMP | RESETTABLE_PEAK + ], [EXPECTED_RESETTABLE_INFO]); + checkPeakColumnInfosAndColor([ + FIELD | DUMP | RESETTABLE_PEAK, + DUMP /* ignored because there's no field */, + 0, + FIELD | DUMP | RESETTABLE_PEAK + ], [EXPECTED_RESETTABLE_INFO]); + + // All non-resettable. + const EXPECTED_NON_RESETTABLE_INFO = { + icon: '\u21A6', + message: 'Peak RSS since process startup. Finer grained peaks require ' + + 'a Linux kernel version \u2265 4.0.' + }; + checkPeakColumnInfosAndColor([ + FIELD | DUMP | NON_RESETTABLE_PEAK + ], [EXPECTED_NON_RESETTABLE_INFO]); + checkPeakColumnInfosAndColor([ + 0, + DUMP | RESETTABLE_PEAK /* ignored because there's no field */, + FIELD | DUMP | NON_RESETTABLE_PEAK, + FIELD | DUMP | NON_RESETTABLE_PEAK + ], [EXPECTED_NON_RESETTABLE_INFO]); + + // Combination (warning). + const EXPECTED_COMBINATION_INFO = { + icon: '\u26A0', + message: 'Both resettable and non-resettable peak RSS values were ' + + 'provided by the process', + color: 'red' + }; + checkPeakColumnInfosAndColor([ + FIELD | DUMP | NON_RESETTABLE_PEAK, + 0, + FIELD | DUMP | RESETTABLE_PEAK, + 0 + ], [EXPECTED_COMBINATION_INFO]); + }); + + test('byteStatColumn', function() { + const c = new ByteStatColumn('Stat', 'bytes', (x => x), + AggregationMode.DIFF); + checkSpanWithColor(c.title, 'Stat', + UsedMemoryColumn.COLOR /* blue (column title) */); + + const HAS_OWN_VM_REGIONS = 1 << 2; + function checkByteStatColumnInfosAndColor( + fieldAndDumpMask, expectedInfos, expectedIsOlderColor) { + checkOverviewColumnInfosAndColor(c, + fieldAndDumpMask, + function(pmd, mask) { + if (mask & HAS_OWN_VM_REGIONS) { + pmd.vmRegions = []; + } + }, + expectedInfos, + expectedIsOlderColor ? + UsedMemoryColumn.OLDER_COLOR /* light blue */ : + UsedMemoryColumn.COLOR /* blue color */); + } + + const EXPECTED_ALL_OLDER_VALUES = { + icon: '\u26AF', + message: 'Older value (only heavy (purple) memory dumps contain ' + + 'memory maps).' + }; + const EXPECTED_SOME_OLDER_VALUES = { + icon: '\u26AF', + message: 'Older value at some selected timestamps (only heavy ' + + '(purple) memory dumps contain memory maps).' + }; + + // No context. + checkOverviewColumnInfosAndColor(c, + [FIELD], + undefined /* no context */, + [] /* no infos */, + UsedMemoryColumn.COLOR /* blue color */); + checkOverviewColumnInfosAndColor(c, + [FIELD, FIELD, 0, FIELD], + undefined /* no context */, + [] /* no infos */, + UsedMemoryColumn.COLOR /* blue color */); + + // All process memory dumps have own VM regions. + checkByteStatColumnInfosAndColor([ + FIELD | DUMP | HAS_OWN_VM_REGIONS + ], [] /* no infos */, false /* blue color */); + checkByteStatColumnInfosAndColor([ + FIELD | DUMP | HAS_OWN_VM_REGIONS, + FIELD | DUMP | HAS_OWN_VM_REGIONS, + 0, + FIELD | DUMP | HAS_OWN_VM_REGIONS + ], [] /* no infos */, false /* blue color */); + + // No process memory dumps have own VM regions. + checkByteStatColumnInfosAndColor([ + FIELD | DUMP + ], [EXPECTED_ALL_OLDER_VALUES], true /* light blue */); + checkByteStatColumnInfosAndColor([ + FIELD | DUMP, + FIELD | DUMP + ], [EXPECTED_ALL_OLDER_VALUES], true /* light blue */); + + // Some process memory dumps don't have own VM regions. + checkByteStatColumnInfosAndColor([ + FIELD | DUMP, + 0, + FIELD | DUMP + ], [EXPECTED_SOME_OLDER_VALUES], true /* light blue */); + checkByteStatColumnInfosAndColor([ + FIELD | DUMP | HAS_OWN_VM_REGIONS, + FIELD | DUMP, + FIELD | DUMP | HAS_OWN_VM_REGIONS + ], [EXPECTED_SOME_OLDER_VALUES], false /* blue */); + }); + + test('allocatorColumn', function() { + const c = new AllocatorColumn('Allocator', 'bytes', (x => x), + AggregationMode.MAX); + checkColorLegend(c.title, 'Allocator'); + checkColor(c.color(undefined /* contexts */), + undefined /* no color (column cells) */); + + const HAS_HEAP_DUMPS = 1 << 2; + const HAS_ALLOCATOR_HEAP_DUMP = 1 << 3; + const MISSING_SIZE = 1 << 4; + function checkAllocatorColumnInfosAndColor(fieldAndDumpMask, + expectedInfos) { + checkOverviewColumnInfosAndColor(c, + fieldAndDumpMask, + function(pmd, mask) { + if (mask & HAS_HEAP_DUMPS) { + pmd.heapDumps = {}; + } + if (mask & HAS_ALLOCATOR_HEAP_DUMP) { + pmd.heapDumps.Allocator = new HeapDump(pmd, 'Allocator'); + } + const mad = new MemoryAllocatorDump(pmd, 'Allocator'); + if (!(mask & MISSING_SIZE)) { + mad.addNumeric('size', + new Scalar(sizeInBytes_smallerIsBetter, 7)); + } + pmd.memoryAllocatorDumps = [mad]; + }, + expectedInfos, + undefined /* no color */); + } + + // No context. + checkOverviewColumnInfosAndColor(c, + [FIELD], + undefined /* no context */, + [] /* no infos */, + undefined /* no color */); + checkOverviewColumnInfosAndColor(c, + [FIELD, FIELD, 0, FIELD], + undefined /* no context */, + [] /* no infos */, + undefined /* no color */); + + // No infos. + checkAllocatorColumnInfosAndColor([ + FIELD | DUMP + ], [] /* no infos */); + checkAllocatorColumnInfosAndColor([ + FIELD | DUMP, + FIELD | DUMP | HAS_HEAP_DUMPS, + 0, + FIELD | DUMP + ], [] /* infos */); + + const EXPECTED_ALL_HAVE_ALLOCATOR_HEAP_DUMP = { + icon: '\u2630', + message: 'Heap dump provided.' + }; + const EXPECTED_SOME_HAVE_ALLOCATOR_HEAP_DUMP = { + icon: '\u2630', + message: 'Heap dump provided at some selected timestamps.' + }; + + // All process memory dumps have heap dumps. + checkAllocatorColumnInfosAndColor([ + FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP + ], [EXPECTED_ALL_HAVE_ALLOCATOR_HEAP_DUMP]); + checkAllocatorColumnInfosAndColor([ + FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP, + FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP, + FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP + ], [EXPECTED_ALL_HAVE_ALLOCATOR_HEAP_DUMP]); + + // Some process memory dumps have heap dumps. + checkAllocatorColumnInfosAndColor([ + 0, + FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP + ], [EXPECTED_SOME_HAVE_ALLOCATOR_HEAP_DUMP]); + checkAllocatorColumnInfosAndColor([ + FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP, + FIELD | DUMP | HAS_HEAP_DUMPS, + FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP + ], [EXPECTED_SOME_HAVE_ALLOCATOR_HEAP_DUMP]); + + const EXPECTED_ALL_MISSING_SIZE = { + icon: '\u26A0', + message: 'Size was not provided.', + color: 'red' + }; + const EXPECTED_SOME_MISSING_SIZE = { + icon: '\u26A0', + message: 'Size was not provided at some selected timestamps.', + color: 'red' + }; + + // All process memory dumps are missing allocator size. + checkAllocatorColumnInfosAndColor([ + FIELD | DUMP | MISSING_SIZE + ], [EXPECTED_ALL_MISSING_SIZE]); + checkAllocatorColumnInfosAndColor([ + FIELD | DUMP | MISSING_SIZE, + FIELD | DUMP | MISSING_SIZE, + FIELD | DUMP | MISSING_SIZE + ], [EXPECTED_ALL_MISSING_SIZE]); + + // Some process memory dumps use Android memtrack PSS fallback. + checkAllocatorColumnInfosAndColor([ + 0, + FIELD | DUMP | MISSING_SIZE + ], [EXPECTED_SOME_MISSING_SIZE]); + checkAllocatorColumnInfosAndColor([ + FIELD | DUMP | MISSING_SIZE, + FIELD | DUMP, + FIELD | DUMP | MISSING_SIZE + ], [EXPECTED_SOME_MISSING_SIZE]); + + // Combination of heap dump and memtrack fallback infos. + checkAllocatorColumnInfosAndColor([ + FIELD | DUMP | MISSING_SIZE | HAS_HEAP_DUMPS | + HAS_ALLOCATOR_HEAP_DUMP + ], [ + EXPECTED_ALL_HAVE_ALLOCATOR_HEAP_DUMP, + EXPECTED_ALL_MISSING_SIZE + ]); + checkAllocatorColumnInfosAndColor([ + FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP, + FIELD | DUMP, + FIELD | DUMP | MISSING_SIZE + ], [ + EXPECTED_SOME_HAVE_ALLOCATOR_HEAP_DUMP, + EXPECTED_SOME_MISSING_SIZE + ]); + }); + + test('tracingColumn', function() { + const c = new TracingColumn('Tracing', 'bytes', (x => x), + AggregationMode.DIFF); + checkSpanWithColor(c.title, 'Tracing', + TracingColumn.COLOR /* expected column title gray color */); + checkColor(c.color(undefined /* contexts */), + TracingColumn.COLOR /* expected column cells gray color */); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_test_utils.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_test_utils.html new file mode 100644 index 00000000000..2f702242140 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_test_utils.html @@ -0,0 +1,593 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color.html"> +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/base/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/global_memory_dump.html"> +<link rel="import" href="/tracing/model/heap_dump.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> +<link rel="import" href="/tracing/model/process_memory_dump.html"> +<link rel="import" href="/tracing/model/vm_region.html"> + +<script> +'use strict'; + +/** + * @fileoverview Helper functions for memory dump analysis sub-view tests. + */ +tr.exportTo('tr.ui.analysis', function() { + const Color = tr.b.Color; + const ColorScheme = tr.b.ColorScheme; + const GlobalMemoryDump = tr.model.GlobalMemoryDump; + const ProcessMemoryDump = tr.model.ProcessMemoryDump; + const VMRegion = tr.model.VMRegion; + const VMRegionClassificationNode = tr.model.VMRegionClassificationNode; + const Scalar = tr.b.Scalar; + const sizeInBytes_smallerIsBetter = + tr.b.Unit.byName.sizeInBytes_smallerIsBetter; + const unitlessNumber_smallerIsBetter = + tr.b.Unit.byName.unitlessNumber_smallerIsBetter; + const HeapDump = tr.model.HeapDump; + const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump; + const addProcessMemoryDump = + tr.model.MemoryDumpTestUtils.addProcessMemoryDump; + const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump; + const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink; + + function createMultipleTestGlobalMemoryDumps() { + const model = tr.c.TestUtils.newModel(function(model) { + const pA = model.getOrCreateProcess(1); + const pB = model.getOrCreateProcess(2); + const pC = model.getOrCreateProcess(3); + const pD = model.getOrCreateProcess(4); + + // ====================================================================== + // First timestamp. + // ====================================================================== + const gmd1 = addGlobalMemoryDump(model, {ts: 42}); + + // Totals and VM regions. + const pmd1A = addProcessMemoryDump(gmd1, pA, {ts: 41}); + pmd1A.totals = {residentBytes: 31457280 /* 30 MiB */}; + pmd1A.vmRegions = VMRegionClassificationNode.fromRegions([ + VMRegion.fromDict({ + startAddress: 1024, + sizeInBytes: 20971520, /* 20 MiB */ + protectionFlags: VMRegion.PROTECTION_FLAG_READ, + mappedFile: '[stack]', + byteStats: { + privateDirtyResident: 8388608, /* 8 MiB */ + sharedCleanResident: 12582912, /* 12 MiB */ + proportionalResident: 10485760 /* 10 MiB */ + } + }) + ]); + + // Everything. + const pmd1B = addProcessMemoryDump(gmd1, pB, {ts: 42}); + pmd1B.totals = { + residentBytes: 20971520, /* 20 MiB */ + peakResidentBytes: 41943040, /* 40 MiB */ + arePeakResidentBytesResettable: false, + privateFootprintBytes: 15728640, /* 15 MiB */ + platformSpecific: { + private_bytes: 10485760 /* 10 MiB */ + } + }; + pmd1B.vmRegions = VMRegionClassificationNode.fromRegions([ + VMRegion.fromDict({ + startAddress: 256, + sizeInBytes: 6000, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_WRITE, + mappedFile: '[stack:20310]', + byteStats: { + proportionalResident: 15728640, /* 15 MiB */ + privateDirtyResident: 1572864, /* 1.5 MiB */ + swapped: 32 /* 32 B */ + } + }), + VMRegion.fromDict({ + startAddress: 100000, + sizeInBytes: 4096, + protectionFlags: VMRegion.PROTECTION_FLAG_READ, + mappedFile: '/usr/lib/libwtf.so', + byteStats: { + proportionalResident: 4194304, /* 4 MiB */ + privateDirtyResident: 0, + swapped: 0 /* 32 B */ + } + }) + ]); + pmd1B.memoryAllocatorDumps = [ + newAllocatorDump(pmd1B, 'malloc', + {numerics: {size: 3145728 /* 3 MiB */}}), + newAllocatorDump(pmd1B, 'v8', {numerics: {size: 5242880 /* 5 MiB */}}), + newAllocatorDump(pmd1B, 'tracing', {numerics: { + size: 1048576 /* 1 MiB */, + resident_size: 1572864 /* 1.5 MiB */ + }}) + ]; + + // Allocator dumps only. + const pmd1C = addProcessMemoryDump(gmd1, pC, {ts: 43}); + pmd1C.memoryAllocatorDumps = (function() { + const oilpanDump = newAllocatorDump(pmd1C, 'oilpan', {numerics: { + size: 3221225472 /* 3 GiB */, + inner_size: 5242880 /* 5 MiB */, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 2015) + }}); + const v8Dump = newAllocatorDump(pmd1C, 'v8', {numerics: { + size: 1073741824 /* 1 GiB */, + inner_size: 2097152 /* 2 MiB */, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 204) + }}); + + addOwnershipLink(v8Dump, oilpanDump); + + return [oilpanDump, v8Dump]; + })(); + pmd1C.heapDumps = { + 'v8': (function() { + const v8HeapDump = new HeapDump(pmd1C, 'v8'); + v8HeapDump.addEntry( + tr.c.TestUtils.newStackTrace(model, + ['V8.Execute', 'UpdateLayoutTree']), + undefined /* sum over all object types */, + 536870912 /* 512 MiB */); + return v8HeapDump; + })() + }; + + // ====================================================================== + // Second timestamp. + // ====================================================================== + const gmd2 = addGlobalMemoryDump(model, {ts: 68}); + + // Everything. + const pmd2A = addProcessMemoryDump(gmd2, pA, {ts: 67}); + pmd2A.totals = {residentBytes: 32505856 /* 31 MiB */}; + pmd2A.vmRegions = VMRegionClassificationNode.fromRegions([ + VMRegion.fromDict({ + startAddress: 1024, + sizeInBytes: 20971520, /* 20 MiB */ + protectionFlags: VMRegion.PROTECTION_FLAG_READ, + mappedFile: '[stack]', + byteStats: { + privateDirtyResident: 8388608, /* 8 MiB */ + sharedCleanResident: 11534336, /* 11 MiB */ + proportionalResident: 11534336 /* 11 MiB */ + } + }), + VMRegion.fromDict({ + startAddress: 104857600, + sizeInBytes: 5242880, /* 5 MiB */ + protectionFlags: VMRegion.PROTECTION_FLAG_EXECUTE, + mappedFile: '/usr/bin/google-chrome', + byteStats: { + privateDirtyResident: 0, + sharedCleanResident: 4194304, /* 4 MiB */ + proportionalResident: 524288 /* 512 KiB */ + } + }) + ]); + pmd2A.memoryAllocatorDumps = [ + newAllocatorDump(pmd2A, 'malloc', {numerics: { + size: 9437184 /* 9 MiB */ + }}), + newAllocatorDump(pmd2A, 'tracing', {numerics: { + size: 2097152 /* 2 MiB */, + resident_size: 2621440 /* 2.5 MiB */ + }}) + ]; + + // Totals and allocator dumps only. + const pmd2B = addProcessMemoryDump(gmd2, pB, {ts: 69}); + pmd2B.totals = { + residentBytes: 19922944, /* 19 MiB */ + peakResidentBytes: 41943040, /* 40 MiB */ + arePeakResidentBytesResettable: false, + privateFootprintBytes: 15728640, /* 15 MiB */ + platformSpecific: { + private_bytes: 8912896 /* 8.5 MiB */ + } + }; + pmd2B.memoryAllocatorDumps = [ + newAllocatorDump(pmd2B, 'malloc', {numerics: { + size: 2621440 /* 2.5 MiB */ + }}), + newAllocatorDump(pmd2B, 'v8', {numerics: { + size: 5242880 /* 5 MiB */ + }}), + newAllocatorDump(pmd2B, 'blink', {numerics: { + size: 7340032 /* 7 MiB */ + }}), + newAllocatorDump(pmd2B, 'oilpan', {numerics: {size: 1}}), + newAllocatorDump(pmd2B, 'tracing', {numerics: { + size: 1572864 /* 1.5 MiB */, + resident_size: 2097152 /* 2 MiB */ + }}), + newAllocatorDump(pmd2B, 'gpu', {numerics: { + memtrack_pss: 524288 /* 512 KiB */ + }}) + ]; + + // Resettable peak total size only. + const pmd2D = addProcessMemoryDump(gmd2, pD, {ts: 71}); + pmd2D.totals = { + peakResidentBytes: 17825792, /* 17 MiB */ + arePeakResidentBytesResettable: true + }; + + // ====================================================================== + // Third timestamp. + // ====================================================================== + const gmd3 = addGlobalMemoryDump(model, {ts: 100}); + + // Everything. + const pmd3B = addProcessMemoryDump(gmd3, pB, {ts: 102}); + pmd3B.totals = { + residentBytes: 18874368, /* 18 MiB */ + peakResidentBytes: 44040192, /* 42 MiB */ + privateFootprintBytes: 15728640, /* 16 MiB */ + arePeakResidentBytesResettable: false, + platformSpecific: { + private_bytes: 7340032 /* 7 MiB */ + } + }; + pmd3B.vmRegions = VMRegionClassificationNode.fromRegions([ + VMRegion.fromDict({ + startAddress: 256, + sizeInBytes: 6000, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_WRITE, + mappedFile: '[stack:20310]', + byteStats: { + proportionalResident: 21495808, /* 20.5 MiB */ + privateDirtyResident: 524288, /* 0.5 MiB */ + swapped: 64 /* 32 B */ + } + }) + ]); + pmd3B.memoryAllocatorDumps = [ + newAllocatorDump(pmd3B, 'malloc', {numerics: { + size: 2883584 /* 2.75 MiB */ + }}), + newAllocatorDump(pmd3B, 'v8', {numerics: { + size: 5767168 /* 5.5 MiB */ + }}), + newAllocatorDump(pmd3B, 'blink', {numerics: { + size: 6291456 /* 7 MiB */ + }}), + newAllocatorDump(pmd3B, 'tracing', {numerics: { + size: 2097152 /* 2 MiB */, + resident_size: 3145728 /* 3 MiB */ + }}), + newAllocatorDump(pmd3B, 'gpu', {numerics: { + size: 1048576 /* 1 MiB */, + memtrack_pss: 786432 /* 768 KiB */ + }}) + ]; + + // Allocator dumps only. + const pmd3C = addProcessMemoryDump(gmd3, pC, {ts: 100}); + pmd3C.memoryAllocatorDumps = (function() { + const oilpanDump = newAllocatorDump(pmd3C, 'oilpan', {numerics: { + size: 3221225472 /* 3 GiB */, + inner_size: 5242880 /* 5 MiB */, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 2015) + }}); + const v8Dump = newAllocatorDump(pmd3C, 'v8', {numerics: { + size: 2147483648 /* 2 GiB */, + inner_size: 2097152 /* 2 MiB */, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 204) + }}); + + addOwnershipLink(v8Dump, oilpanDump); + + return [oilpanDump, v8Dump]; + })(); + pmd3C.heapDumps = { + 'v8': (function() { + const v8HeapDump = new HeapDump(pmd1C, 'v8'); + v8HeapDump.addEntry( + tr.c.TestUtils.newStackTrace(model, + ['V8.Execute', 'UpdateLayoutTree']), + undefined /* sum over all object types */, + 268435456 /* 256 MiB */); + v8HeapDump.addEntry( + tr.c.TestUtils.newStackTrace(model, + ['V8.Execute', 'FrameView::layout']), + undefined /* sum over all object types */, + 134217728 /* 128 MiB */); + return v8HeapDump; + })() + }; + + // Resettable peak total size only. + const pmd3D = addProcessMemoryDump(gmd3, pD, {ts: 99}); + pmd3D.totals = { + peakResidentBytes: 17825792, /* 17 MiB */ + arePeakResidentBytesResettable: true + }; + }); + + return model.globalMemoryDumps; + } + + function createSingleTestGlobalMemoryDump() { + return createMultipleTestGlobalMemoryDumps()[1]; + } + + function createMultipleTestProcessMemoryDumps() { + return createMultipleTestGlobalMemoryDumps().map(function(gmd) { + return gmd.processMemoryDumps[2]; + }); + } + + function createSingleTestProcessMemoryDump() { + return createMultipleTestProcessMemoryDumps()[1]; + } + + function checkNumericFields(row, column, expectedValues, expectedUnit) { + let fields; + if (column === undefined) { + fields = row; + } else { + fields = column.fields(row); + } + + if (expectedValues === undefined) { + assert.isUndefined(fields); + return; + } + + assert.lengthOf(fields, expectedValues.length); + for (let i = 0; i < fields.length; i++) { + const field = fields[i]; + const expectedValue = expectedValues[i]; + if (expectedValue === undefined) { + assert.isUndefined(field); + } else { + assert.isDefined(expectedUnit); // Test sanity check. + assert.instanceOf(field, Scalar); + assert.strictEqual(field.value, expectedValue); + assert.strictEqual(field.unit, expectedUnit); + } + } + } + + function checkSizeNumericFields(row, column, expectedValues) { + checkNumericFields(row, column, expectedValues, + sizeInBytes_smallerIsBetter); + } + + function checkStringFields(row, column, expectedStrings) { + const fields = column.fields(row); + + if (expectedStrings === undefined) { + assert.isUndefined(fields); + return; + } + + assert.deepEqual(Array.from(fields), expectedStrings); + } + + /** + * Check the titles, types and aggregation modes of a list of columns. + * expectedColumns is a list of dictionaries with the following fields: + * + * - title: Either the expected title (string), or a matcher for it + * (function that accepts the actual title as its argument). + * - type: The expected class of the column. + * - noAggregation: If true, the column is expected to have no aggregation + * mode (regardless of expectedAggregationMode). + */ + function checkColumns(columns, expectedColumns, expectedAggregationMode) { + assert.lengthOf(columns, expectedColumns.length); + for (let i = 0; i < expectedColumns.length; i++) { + const actualColumn = columns[i]; + const expectedColumn = expectedColumns[i]; + const expectedTitle = expectedColumn.title; + if (typeof expectedTitle === 'function') { + expectedTitle(actualColumn.title); // Custom title matcher. + } else if (actualColumn.title.innerText) { + // HTML title. + assert.strictEqual(actualColumn.title.innerText, expectedTitle); + } else { + assert.strictEqual(actualColumn.title, expectedTitle); // String title. + } + assert.instanceOf(actualColumn, expectedColumn.type); + assert.strictEqual(actualColumn.aggregationMode, + expectedColumn.noAggregation ? undefined : expectedAggregationMode); + } + } + + function checkColumnInfosAndColor( + column, fields, contexts, expectedInfos, expectedColorReservedName) { + // Test sanity checks. + assert.isDefined(fields); + if (contexts !== undefined) { + assert.lengthOf(contexts, fields.length); + } + + // Check infos. + const infos = []; + column.addInfos(fields, contexts, infos); + assert.lengthOf(infos, expectedInfos.length); + for (let i = 0; i < expectedInfos.length; i++) { + assert.deepEqual(infos[i], expectedInfos[i]); + } + + // Check color. + const actualColor = typeof column.color === 'function' ? + column.color(fields, contexts) : + column.color; + checkColor(actualColor, expectedColorReservedName); + } + + function checkColor(actualColorString, expectedColorString) { + if (actualColorString === undefined) { + assert.isUndefined(expectedColorString); + return; + } + const actualColor = Color.fromString(actualColorString); + const expectedColor = Color.fromString(expectedColorString); + assert.deepEqual(actualColor, expectedColor); + } + + function createAndCheckEmptyPanes( + test, paneTagName, propertyName, opt_callback) { + // Unset property. + const unsetViewEl = createTestPane(paneTagName); + unsetViewEl.rebuild(); + assert.isUndefined(unsetViewEl.createChildPane()); + test.addHTMLOutput(unsetViewEl); + + // Undefined property. + const undefinedViewEl = createTestPane(paneTagName); + undefinedViewEl[propertyName] = undefined; + undefinedViewEl.rebuild(); + assert.isUndefined(undefinedViewEl.createChildPane()); + test.addHTMLOutput(undefinedViewEl); + + // Empty property. + const emptyViewEl = createTestPane(paneTagName); + emptyViewEl[propertyName] = []; + emptyViewEl.rebuild(); + assert.isUndefined(undefinedViewEl.createChildPane()); + test.addHTMLOutput(emptyViewEl); + + // Check that all the panes have the same dimensions. + const unsetBounds = unsetViewEl.getBoundingClientRect(); + const undefinedBounds = undefinedViewEl.getBoundingClientRect(); + const emptyBounds = emptyViewEl.getBoundingClientRect(); + assert.strictEqual(undefinedBounds.width, unsetBounds.width); + assert.strictEqual(emptyBounds.width, unsetBounds.width); + assert.strictEqual(undefinedBounds.height, unsetBounds.height); + assert.strictEqual(emptyBounds.height, unsetBounds.height); + + // Custom checks (if provided). + if (opt_callback) { + opt_callback(unsetViewEl); + opt_callback(undefinedViewEl); + opt_callback(emptyViewEl); + } + } + + function createTestPane(tagName) { + const paneEl = document.createElement(tagName); + + // Store a list of requested child panes (for inspection in tests). + paneEl.requestedChildPanes = []; + paneEl.addEventListener('request-child-pane-change', function() { + paneEl.requestedChildPanes.push(paneEl.createChildPane()); + }); + + paneEl.createChildPane = function() { + const childPaneBuilder = this.childPaneBuilder; + if (childPaneBuilder === undefined) return undefined; + return childPaneBuilder(); + }; + + return paneEl; + } + + // TODO(petrcermak): Consider moving this to tracing/ui/base/dom_helpers.html. + function isElementDisplayed(element) { + const style = getComputedStyle(element); + const displayed = style.display; + if (displayed === undefined) return true; + return displayed.indexOf('none') === -1; + } + + /** + * Convert a list of ContainerMemoryDump(s) to a list of dictionaries of the + * underlying ProcessMemoryDump(s). + */ + function convertToProcessMemoryDumps(containerMemoryDumps) { + return containerMemoryDumps.map(function(containerMemoryDump) { + return containerMemoryDump.processMemoryDumps; + }); + } + + /** + * Extract a chronological list of ProcessMemoryDump(s) (for a given process) + * from a chronological list of dictionaries of ProcessMemoryDump(s). + */ + function extractProcessMemoryDumps(processMemoryDumps, pid) { + return processMemoryDumps.map(function(memoryDumps) { + return memoryDumps[pid]; + }); + } + + /** + * Extract a chronological list of lists of VMRegion(s) (for a given process) + * from a chronological list of dictionaries of ProcessMemoryDump(s). + */ + function extractVmRegions(processMemoryDumps, pid) { + return processMemoryDumps.map(function(memoryDumps) { + const processMemoryDump = memoryDumps[pid]; + if (processMemoryDump === undefined) return undefined; + return processMemoryDump.mostRecentVmRegions; + }); + } + + /** + * Extract a chronological list of MemoryAllocatorDump(s) (for a given + * process and allocator name) from a chronological list of dictionaries of + * ProcessMemoryDump(s). + */ + function extractMemoryAllocatorDumps(processMemoryDumps, pid, allocatorName) { + return processMemoryDumps.map(function(memoryDumps) { + const processMemoryDump = memoryDumps[pid]; + if (processMemoryDump === undefined) return undefined; + return processMemoryDump.getMemoryAllocatorDumpByFullName(allocatorName); + }); + } + + /** + * Extract a chronological list of HeapDump(s) (for a given process and + * allocator name) from a chronological list of dictionaries of + * ProcessMemoryDump(s). + */ + function extractHeapDumps(processMemoryDumps, pid, allocatorName) { + return processMemoryDumps.map(function(memoryDumps) { + const processMemoryDump = memoryDumps[pid]; + if (processMemoryDump === undefined || + processMemoryDump.heapDumps === undefined) { + return undefined; + } + return processMemoryDump.heapDumps[allocatorName]; + }); + } + + return { + createSingleTestGlobalMemoryDump, + createMultipleTestGlobalMemoryDumps, + createSingleTestProcessMemoryDump, + createMultipleTestProcessMemoryDumps, + checkNumericFields, + checkSizeNumericFields, + checkStringFields, + checkColumns, + checkColumnInfosAndColor, + checkColor, + createAndCheckEmptyPanes, + createTestPane, + isElementDisplayed, + convertToProcessMemoryDumps, + extractProcessMemoryDumps, + extractVmRegions, + extractMemoryAllocatorDumps, + extractHeapDumps, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util.html new file mode 100644 index 00000000000..654ce0ea73f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util.html @@ -0,0 +1,915 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/scalar.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<script> +'use strict'; + +/** + * @fileoverview Helper code for memory dump sub-views. + */ +tr.exportTo('tr.ui.analysis', function() { + const NO_BREAK_SPACE = String.fromCharCode(160); + const RIGHTWARDS_ARROW = String.fromCharCode(8594); + + const COLLATOR = new Intl.Collator(undefined, {numeric: true}); + + /** + * A table column for displaying memory dump row titles. + * + * @constructor + */ + function TitleColumn(title) { + this.title = title; + } + + TitleColumn.prototype = { + supportsCellSelection: false, + + /** + * Get the title associated with a given row. + * + * This method will decorate the title with color and '+++'/'---' prefix if + * appropriate (as determined by the optional row.contexts field). + * Examples: + * + * +----------------------+-----------------+--------+--------+ + * | Contexts provided at | Interpretation | Prefix | Color | + * +----------------------+-----------------+--------+--------+ + * | 1111111111 | always present | | | + * | 0000111111 | added | +++ | red | + * | 1111111000 | deleted | --- | green | + * | 1100111111* | flaky | | purple | + * | 0001001111 | added + flaky | +++ | purple | + * | 1111100010 | deleted + flaky | --- | purple | + * +----------------------+-----------------+--------+--------+ + * + * *) This means that, given a selection of 10 memory dumps, a particular + * row (e.g. a process) was present in the first 2 and last 6 of them + * (but not in the third and fourth dump). + * + * This method should therefore NOT be overriden by subclasses. The + * formatTitle method should be overriden instead when necessary. + */ + value(row) { + const formattedTitle = this.formatTitle(row); + + const contexts = row.contexts; + if (contexts === undefined || contexts.length === 0) { + return formattedTitle; + } + + // Determine if the row was provided in the first and last row and how + // many times it changed between being provided and not provided. + const firstContext = contexts[0]; + const lastContext = contexts[contexts.length - 1]; + let changeDefinedContextCount = 0; + for (let i = 1; i < contexts.length; i++) { + if ((contexts[i] === undefined) !== (contexts[i - 1] === undefined)) { + changeDefinedContextCount++; + } + } + + // Determine the color and prefix of the title. + let color = undefined; + let prefix = undefined; + if (!firstContext && lastContext) { + // The row was added. + color = 'red'; + prefix = '+++'; + } else if (firstContext && !lastContext) { + // The row was removed. + color = 'green'; + prefix = '---'; + } + if (changeDefinedContextCount > 1) { + // The row was flaky (added/removed more than once). + color = 'purple'; + } + + if (color === undefined && prefix === undefined) { + return formattedTitle; + } + + const titleEl = document.createElement('span'); + if (prefix !== undefined) { + const prefixEl = tr.ui.b.createSpan({textContent: prefix}); + // Enforce same width of '+++' and '---'. + prefixEl.style.fontFamily = 'monospace'; + Polymer.dom(titleEl).appendChild(prefixEl); + Polymer.dom(titleEl).appendChild( + tr.ui.b.asHTMLOrTextNode(NO_BREAK_SPACE)); + } + if (color !== undefined) { + titleEl.style.color = color; + } + Polymer.dom(titleEl).appendChild( + tr.ui.b.asHTMLOrTextNode(formattedTitle)); + return titleEl; + }, + + /** + * Format the title associated with a given row. This method is intended to + * be overriden by subclasses. + */ + formatTitle(row) { + return row.title; + }, + + cmp(rowA, rowB) { + return COLLATOR.compare(rowA.title, rowB.title); + } + }; + + /** + * Abstract table column for displaying memory dump data. + * + * @constructor + */ + function MemoryColumn(name, cellPath, aggregationMode) { + this.name = name; + this.cellPath = cellPath; + this.shouldSetContextGroup = false; + + // See MemoryColumn.AggregationMode enum in this file. + this.aggregationMode = aggregationMode; + } + + /** + * Construct columns from cells in a hierarchy of rows and a list of rules. + * + * The list of rules contains objects with three fields: + * + * condition: Optional string or regular expression matched against the + * name of a cell. If omitted, the rule will match any cell. + * importance: Mandatory number which determines the final order of the + * columns. The column with the highest importance will be first in the + * returned array. + * columnConstructor: Mandatory memory column constructor. + * + * Example: + * + * const importanceRules = [ + * { + * condition: 'page_size', + * columnConstructor: NumericMemoryColumn, + * importance: 8 + * }, + * { + * condition: /size/, + * columnConstructor: CustomNumericMemoryColumn, + * importance: 10 + * }, + * { + * // No condition: matches all columns. + * columnConstructor: NumericMemoryColumn, + * importance: 9 + * } + * ]; + * + * Given a name of a cell, the corresponding column constructor and + * importance are determined by the first rule whose condition matches the + * column's name. For example, given a cell with name 'inner_size', the + * corresponding column will be constructed using CustomNumericMemoryColumn + * and its importance (for sorting purposes) will be 10 (second rule). + * + * After columns are constructed for all cell names, they are sorted in + * descending order of importance and the resulting list is returned. In the + * example above, the constructed columns will be sorted into three groups as + * follows: + * + * [most important, left in the resulting table] + * 1. columns whose name contains 'size' excluding 'page_size' because it + * would have already matched the first rule (Note that string matches + * must be exact so a column named 'page_size2' would not match the + * first rule and would therefore belong to this group). + * 2. columns whose name does not contain 'size'. + * 3. columns whose name is 'page_size'. + * [least important, right in the resulting table] + * + * where columns will be sorted alphabetically within each group. + * + * @param {!Array.<!Object>} rows + * @param {!Object} config + * @param {string} config.cellKey + * @param {!MemoryColumn.AggregationMode=} config.aggregationMode + * @param {!Array.<!{ + * condition: (string|!RegExp)=, + * importance: number, + * columnConstructor: !function(new: MemoryColumn, ...)=, + * shouldSetContextGroup: boolean= + * }>} config.rules + */ + MemoryColumn.fromRows = function(rows, config) { + // Recursively find the names of all cells of the rows (and their sub-rows). + const cellNames = new Set(); + function gatherCellNames(rows) { + rows.forEach(function(row) { + if (row === undefined) return; + const fieldCells = row[config.cellKey]; + if (fieldCells !== undefined) { + for (const [fieldName, fieldCell] of Object.entries(fieldCells)) { + if (fieldCell === undefined || fieldCell.fields === undefined) { + continue; + } + cellNames.add(fieldName); + } + } + const subRows = row.subRows; + if (subRows !== undefined) { + gatherCellNames(subRows); + } + }); + } + gatherCellNames(rows); + + // Based on the provided list of rules, construct the columns and calculate + // their importance. + const positions = []; + cellNames.forEach(function(cellName) { + const cellPath = [config.cellKey, cellName]; + const matchingRule = MemoryColumn.findMatchingRule( + cellName, config.rules); + const constructor = matchingRule.columnConstructor; + const column = new constructor( + cellName, cellPath, config.aggregationMode); + column.shouldSetContextGroup = !!config.shouldSetContextGroup; + positions.push({ + importance: matchingRule.importance, + column + }); + }); + + positions.sort(function(a, b) { + // Sort columns with the same importance alphabetically. + if (a.importance === b.importance) { + return COLLATOR.compare(a.column.name, b.column.name); + } + + // Sort columns in descending order of importance. + return b.importance - a.importance; + }); + + return positions.map(function(position) { return position.column; }); + }; + + MemoryColumn.spaceEqually = function(columns) { + const columnWidth = (100 / columns.length).toFixed(3) + '%'; + columns.forEach(function(column) { + column.width = columnWidth; + }); + }; + + MemoryColumn.findMatchingRule = function(name, rules) { + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + if (MemoryColumn.nameMatchesCondition(name, rule.condition)) { + return rule; + } + } + return undefined; + }; + + MemoryColumn.nameMatchesCondition = function(name, condition) { + // Rules without conditions match all columns. + if (condition === undefined) return true; + + // String conditions must match the column name exactly. + if (typeof(condition) === 'string') return name === condition; + + // If the condition is not a string, assume it is a RegExp. + return condition.test(name); + }; + + /** @enum */ + MemoryColumn.AggregationMode = { + DIFF: 0, + MAX: 1 + }; + + MemoryColumn.SOME_TIMESTAMPS_INFO_QUANTIFIER = 'at some selected timestamps'; + + MemoryColumn.prototype = { + get title() { + return this.name; + }, + + cell(row) { + let cell = row; + const cellPath = this.cellPath; + for (let i = 0; i < cellPath.length; i++) { + if (cell === undefined) return undefined; + cell = cell[cellPath[i]]; + } + return cell; + }, + + aggregateCells(row, subRows) { + // No generic aggregation. + }, + + fields(row) { + const cell = this.cell(row); + if (cell === undefined) return undefined; + return cell.fields; + }, + + /** + * Format a cell associated with this column from the given row. This + * method is not intended to be overriden. + */ + value(row) { + const fields = this.fields(row); + if (this.hasAllRelevantFieldsUndefined(fields)) return ''; + + // Determine the color and infos of the resulting element. + const contexts = row.contexts; + const color = this.color(fields, contexts); + const infos = []; + this.addInfos(fields, contexts, infos); + + // Format the actual fields. + const formattedFields = this.formatFields(fields); + + // If no color is specified and there are no infos, there is no need to + // wrap the value in a span element.# + if ((color === undefined || formattedFields === '') && + infos.length === 0) { + return formattedFields; + } + + const fieldEl = document.createElement('span'); + fieldEl.style.display = 'flex'; + fieldEl.style.alignItems = 'center'; + fieldEl.style.justifyContent = 'flex-end'; + Polymer.dom(fieldEl).appendChild( + tr.ui.b.asHTMLOrTextNode(formattedFields)); + + // Add info icons with tooltips. + infos.forEach(function(info) { + const infoEl = document.createElement('span'); + infoEl.style.paddingLeft = '4px'; + infoEl.style.cursor = 'help'; + infoEl.style.fontWeight = 'bold'; + Polymer.dom(infoEl).textContent = info.icon; + if (info.color !== undefined) { + infoEl.style.color = info.color; + } + infoEl.title = info.message; + Polymer.dom(fieldEl).appendChild(infoEl); + }, this); + + // Set the color of the element. + if (color !== undefined) { + fieldEl.style.color = color; + } + + return fieldEl; + }, + + /** + * Returns true iff all fields of a row which are relevant for the current + * aggregation mode (e.g. first and last field for diff mode) are undefined. + */ + hasAllRelevantFieldsUndefined(fields) { + if (fields === undefined) return true; + + switch (this.aggregationMode) { + case MemoryColumn.AggregationMode.DIFF: + // Only the first and last field are relevant. + return fields[0] === undefined && + fields[fields.length - 1] === undefined; + + case MemoryColumn.AggregationMode.MAX: + default: + // All fields are relevant. + return fields.every(function(field) { return field === undefined; }); + } + }, + + /** + * Get the color of the given fields formatted by this column. At least one + * field relevant for the current aggregation mode is guaranteed to be + * defined. + */ + color(fields, contexts) { + return undefined; + }, + + /** + * Format an arbitrary number of fields. At least one field relevant for + * the current aggregation mode is guaranteed to be defined. + */ + formatFields(fields) { + if (fields.length === 1) { + return this.formatSingleField(fields[0]); + } + return this.formatMultipleFields(fields); + }, + + /** + * Format a single defined field. + * + * This method is intended to be overriden by field type specific columns + * (e.g. show '1.0 KiB' instead of '1024' for Scalar(s) representing + * bytes). + */ + formatSingleField(field) { + throw new Error('Not implemented'); + }, + + /** + * Format multiple fields. At least one field relevant for the current + * aggregation mode is guaranteed to be defined. + * + * The aggregation mode specializations of this method (e.g. + * formatMultipleFieldsDiff) are intended to be overriden by field type + * specific columns. + */ + formatMultipleFields(fields) { + switch (this.aggregationMode) { + case MemoryColumn.AggregationMode.DIFF: + return this.formatMultipleFieldsDiff( + fields[0], fields[fields.length - 1]); + + case MemoryColumn.AggregationMode.MAX: + return this.formatMultipleFieldsMax(fields); + + default: + return tr.ui.b.createSpan({ + textContent: '(unsupported aggregation mode)', + italic: true + }); + } + }, + + formatMultipleFieldsDiff(firstField, lastField) { + throw new Error('Not implemented'); + }, + + formatMultipleFieldsMax(fields) { + return this.formatSingleField(this.getMaxField(fields)); + }, + + cmp(rowA, rowB) { + const fieldsA = this.fields(rowA); + const fieldsB = this.fields(rowB); + + // Sanity check. + if (fieldsA !== undefined && fieldsB !== undefined && + fieldsA.length !== fieldsB.length) { + throw new Error('Different number of fields'); + } + + // Handle empty fields. + const undefinedA = this.hasAllRelevantFieldsUndefined(fieldsA); + const undefinedB = this.hasAllRelevantFieldsUndefined(fieldsB); + if (undefinedA && undefinedB) return 0; + if (undefinedA) return -1; + if (undefinedB) return 1; + + return this.compareFields(fieldsA, fieldsB); + }, + + /** + * Compare a pair of single or multiple fields. At least one field relevant + * for the current aggregation mode is guaranteed to be defined in each of + * the two lists. + */ + compareFields(fieldsA, fieldsB) { + if (fieldsA.length === 1) { + return this.compareSingleFields(fieldsA[0], fieldsB[0]); + } + return this.compareMultipleFields(fieldsA, fieldsB); + }, + + /** + * Compare a pair of single defined fields. + * + * This method is intended to be overriden by field type specific columns. + */ + compareSingleFields(fieldA, fieldB) { + throw new Error('Not implemented'); + }, + + /** + * Compare a pair of multiple fields. At least one field relevant for the + * current aggregation mode is guaranteed to be defined in each of the two + * lists. + * + * The aggregation mode specializations of this method (e.g. + * compareMultipleFieldsDiff) are intended to be overriden by field type + * specific columns. + */ + compareMultipleFields(fieldsA, fieldsB) { + switch (this.aggregationMode) { + case MemoryColumn.AggregationMode.DIFF: + return this.compareMultipleFieldsDiff( + fieldsA[0], fieldsA[fieldsA.length - 1], + fieldsB[0], fieldsB[fieldsB.length - 1]); + + case MemoryColumn.AggregationMode.MAX: + return this.compareMultipleFieldsMax(fieldsA, fieldsB); + + default: + return 0; + } + }, + + compareMultipleFieldsDiff(firstFieldA, lastFieldA, firstFieldB, + lastFieldB) { + throw new Error('Not implemented'); + }, + + compareMultipleFieldsMax(fieldsA, fieldsB) { + return this.compareSingleFields( + this.getMaxField(fieldsA), this.getMaxField(fieldsB)); + }, + + getMaxField(fields) { + return fields.reduce(function(accumulator, field) { + if (field === undefined) { + return accumulator; + } + if (accumulator === undefined || + this.compareSingleFields(field, accumulator) > 0) { + return field; + } + return accumulator; + }.bind(this), undefined); + }, + + addInfos(fields, contexts, infos) { + // No generic infos. + }, + + getImportance(importanceRules) { + if (importanceRules.length === 0) return 0; + + // Find the first matching rule. + const matchingRule = + MemoryColumn.findMatchingRule(this.name, importanceRules); + if (matchingRule !== undefined) { + return matchingRule.importance; + } + + // No matching rule. Return lower importance than all rules. + let minImportance = importanceRules[0].importance; + for (let i = 1; i < importanceRules.length; i++) { + minImportance = Math.min(minImportance, importanceRules[i].importance); + } + return minImportance - 1; + } + }; + + /** + * @constructor + */ + function StringMemoryColumn(name, cellPath, aggregationMode) { + MemoryColumn.call(this, name, cellPath, aggregationMode); + } + + StringMemoryColumn.prototype = { + __proto__: MemoryColumn.prototype, + + formatSingleField(string) { + return string; + }, + + formatMultipleFieldsDiff(firstString, lastString) { + if (firstString === undefined) { + // String was added ("+NEW_VALUE" in red). + const spanEl = tr.ui.b.createSpan({color: 'red'}); + Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode('+')); + Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode( + this.formatSingleField(lastString))); + return spanEl; + } else if (lastString === undefined) { + // String was removed ("-OLD_VALUE" in green). + const spanEl = tr.ui.b.createSpan({color: 'green'}); + Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode('-')); + Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode( + this.formatSingleField(firstString))); + return spanEl; + } else if (firstString === lastString) { + // String didn't change ("VALUE" with unchanged color). + return this.formatSingleField(firstString); + } + // String changed ("OLD_VALUE -> NEW_VALUE" in orange). + const spanEl = tr.ui.b.createSpan({color: 'DarkOrange'}); + Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode( + this.formatSingleField(firstString))); + Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode( + ' ' + RIGHTWARDS_ARROW + ' ')); + Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode( + this.formatSingleField(lastString))); + return spanEl; + }, + + compareSingleFields(stringA, stringB) { + return COLLATOR.compare(stringA, stringB); + }, + + compareMultipleFieldsDiff(firstStringA, lastStringA, firstStringB, + lastStringB) { + // If one of the strings was added (and the other one wasn't), mark the + // corresponding diff as greater. + if (firstStringA === undefined && firstStringB !== undefined) { + return 1; + } + if (firstStringA !== undefined && firstStringB === undefined) { + return -1; + } + + // If both strings were added, compare the last values (greater last + // value implies greater diff). + if (firstStringA === undefined && firstStringB === undefined) { + return this.compareSingleFields(lastStringA, lastStringB); + } + + // If one of the strings was removed (and the other one wasn't), mark the + // corresponding diff as lower. + if (lastStringA === undefined && lastStringB !== undefined) { + return -1; + } + if (lastStringA !== undefined && lastStringB === undefined) { + return 1; + } + + // If both strings were removed, compare the first values (greater first + // value implies smaller (!) diff). + if (lastStringA === undefined && lastStringB === undefined) { + return this.compareSingleFields(firstStringB, firstStringA); + } + + const areStringsAEqual = firstStringA === lastStringA; + const areStringsBEqual = firstStringB === lastStringB; + + // Consider diffs of strings that did not change to be smaller than diffs + // of strings that did change. + if (areStringsAEqual && areStringsBEqual) return 0; + if (areStringsAEqual) return -1; + if (areStringsBEqual) return 1; + + // Both strings changed. We are unable to determine the ordering of the + // diffs. + return 0; + } + }; + + /** + * @constructor + */ + function NumericMemoryColumn(name, cellPath, aggregationMode) { + MemoryColumn.call(this, name, cellPath, aggregationMode); + } + + // Avoid tiny positive/negative diffs (displayed in the UI as '+0.0 B' and + // '-0.0 B') due to imprecise floating-point arithmetic by treating all diffs + // within the (-DIFF_EPSILON, DIFF_EPSILON) range as zeros. + NumericMemoryColumn.DIFF_EPSILON = 0.0001; + + NumericMemoryColumn.prototype = { + __proto__: MemoryColumn.prototype, + + align: tr.ui.b.TableFormat.ColumnAlignment.RIGHT, + + aggregateCells(row, subRows) { + const subRowCells = subRows.map(this.cell, this); + + // Determine if there is at least one defined numeric in the sub-row + // cells and the timestamp count. + let hasDefinedSubRowNumeric = false; + let timestampCount = undefined; + subRowCells.forEach(function(subRowCell) { + if (subRowCell === undefined) return; + + const subRowNumerics = subRowCell.fields; + if (subRowNumerics === undefined) return; + + if (timestampCount === undefined) { + timestampCount = subRowNumerics.length; + } else if (timestampCount !== subRowNumerics.length) { + throw new Error('Sub-rows have different numbers of timestamps'); + } + + if (hasDefinedSubRowNumeric) { + return; // Avoid unnecessary traversals of the numerics. + } + hasDefinedSubRowNumeric = subRowNumerics.some(function(numeric) { + return numeric !== undefined; + }); + }); + if (!hasDefinedSubRowNumeric) { + return; // No numeric to aggregate. + } + + // Get or create the row cell. + const cellPath = this.cellPath; + let rowCell = row; + for (let i = 0; i < cellPath.length; i++) { + const nextStepName = cellPath[i]; + let nextStep = rowCell[nextStepName]; + if (nextStep === undefined) { + if (i < cellPath.length - 1) { + nextStep = {}; + } else { + nextStep = new MemoryCell(undefined); + } + rowCell[nextStepName] = nextStep; + } + rowCell = nextStep; + } + if (rowCell.fields === undefined) { + rowCell.fields = new Array(timestampCount); + } else if (rowCell.fields.length !== timestampCount) { + throw new Error( + 'Row has a different number of timestamps than sub-rows'); + } + + for (let i = 0; i < timestampCount; i++) { + if (rowCell.fields[i] !== undefined) continue; + rowCell.fields[i] = tr.model.MemoryAllocatorDump.aggregateNumerics( + subRowCells.map(function(subRowCell) { + if (subRowCell === undefined || subRowCell.fields === undefined) { + return undefined; + } + return subRowCell.fields[i]; + })); + } + }, + + formatSingleField(numeric) { + return tr.v.ui.createScalarSpan(numeric, { + context: this.getFormattingContext(numeric.unit), + contextGroup: this.shouldSetContextGroup ? this.name : undefined, + inline: true, + }); + }, + + getFormattingContext(unit) { + return undefined; + }, + + formatMultipleFieldsDiff(firstNumeric, lastNumeric) { + return this.formatSingleField( + this.getDiffField_(firstNumeric, lastNumeric)); + }, + + compareSingleFields(numericA, numericB) { + return numericA.value - numericB.value; + }, + + compareMultipleFieldsDiff(firstNumericA, lastNumericA, + firstNumericB, lastNumericB) { + return this.getDiffFieldValue_(firstNumericA, lastNumericA) - + this.getDiffFieldValue_(firstNumericB, lastNumericB); + }, + + getDiffField_(firstNumeric, lastNumeric) { + const definedNumeric = firstNumeric || lastNumeric; + return new tr.b.Scalar(definedNumeric.unit.correspondingDeltaUnit, + this.getDiffFieldValue_(firstNumeric, lastNumeric)); + }, + + getDiffFieldValue_(firstNumeric, lastNumeric) { + const firstValue = firstNumeric === undefined ? 0 : firstNumeric.value; + const lastValue = lastNumeric === undefined ? 0 : lastNumeric.value; + const diff = lastValue - firstValue; + return Math.abs(diff) < NumericMemoryColumn.DIFF_EPSILON ? 0 : diff; + } + }; + + /** + * @constructor + */ + function MemoryCell(fields) { + this.fields = fields; + } + + MemoryCell.extractFields = function(cell) { + if (cell === undefined) return undefined; + return cell.fields; + }; + + /** Limit for the number of sub-rows for recursive table row expansion. */ + const RECURSIVE_EXPANSION_MAX_VISIBLE_ROW_COUNT = 10; + + function expandTableRowsRecursively(table) { + let currentLevelRows = table.tableRows; + let totalVisibleRowCount = currentLevelRows.length; + + while (currentLevelRows.length > 0) { + // Calculate the total number of sub-rows on the current level. + let nextLevelRowCount = 0; + currentLevelRows.forEach(function(currentLevelRow) { + const subRows = currentLevelRow.subRows; + if (subRows === undefined || subRows.length === 0) return; + nextLevelRowCount += subRows.length; + }); + + // Determine whether expanding all rows on the current level would cause + // the total number of visible rows go over the limit. + if (totalVisibleRowCount + nextLevelRowCount > + RECURSIVE_EXPANSION_MAX_VISIBLE_ROW_COUNT) { + break; + } + + // Expand all rows on the current level and gather their sub-rows. + const nextLevelRows = new Array(nextLevelRowCount); + let nextLevelRowIndex = 0; + currentLevelRows.forEach(function(currentLevelRow) { + const subRows = currentLevelRow.subRows; + if (subRows === undefined || subRows.length === 0) return; + table.setExpandedForTableRow(currentLevelRow, true); + subRows.forEach(function(subRow) { + nextLevelRows[nextLevelRowIndex++] = subRow; + }); + }); + + // Update the total number of visible rows and progress to the next level. + totalVisibleRowCount += nextLevelRowCount; + currentLevelRows = nextLevelRows; + } + } + + function aggregateTableRowCellsRecursively(row, columns, opt_predicate) { + const subRows = row.subRows; + if (subRows === undefined || subRows.length === 0) return; + + subRows.forEach(function(subRow) { + aggregateTableRowCellsRecursively(subRow, columns, opt_predicate); + }); + + if (opt_predicate === undefined || opt_predicate(row.contexts)) { + aggregateTableRowCells(row, subRows, columns); + } + } + + function aggregateTableRowCells(row, subRows, columns) { + columns.forEach(function(column) { + if (!(column instanceof MemoryColumn)) return; + column.aggregateCells(row, subRows); + }); + } + + function createCells(timeToValues, valueFieldsGetter, opt_this) { + opt_this = opt_this || this; + const fieldNameToFields = tr.b.invertArrayOfDicts( + timeToValues, valueFieldsGetter, opt_this); + const result = {}; + for (const [fieldName, fields] of Object.entries(fieldNameToFields)) { + result[fieldName] = new tr.ui.analysis.MemoryCell(fields); + } + return result; + } + + function createWarningInfo(message) { + return { + message, + icon: String.fromCharCode(9888), + color: 'red' + }; + } + + // TODO(petrcermak): Use a context manager instead + // (https://github.com/catapult-project/catapult/issues/2420). + function DetailsNumericMemoryColumn(name, cellPath, aggregationMode) { + NumericMemoryColumn.call(this, name, cellPath, aggregationMode); + } + + DetailsNumericMemoryColumn.prototype = { + __proto__: NumericMemoryColumn.prototype, + + getFormattingContext(unit) { + if (unit.baseUnit === tr.b.Unit.byName.sizeInBytes) { + return { unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI }; + } + return undefined; + } + }; + + return { + TitleColumn, + MemoryColumn, + StringMemoryColumn, + NumericMemoryColumn, + MemoryCell, + expandTableRowsRecursively, + aggregateTableRowCellsRecursively, + aggregateTableRowCells, + createCells, + createWarningInfo, + DetailsNumericMemoryColumn, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util_test.html new file mode 100644 index 00000000000..859f78433d6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util_test.html @@ -0,0 +1,1241 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const TitleColumn = tr.ui.analysis.TitleColumn; + const MemoryColumn = tr.ui.analysis.MemoryColumn; + const AggregationMode = MemoryColumn.AggregationMode; + const StringMemoryColumn = tr.ui.analysis.StringMemoryColumn; + const NumericMemoryColumn = tr.ui.analysis.NumericMemoryColumn; + const MemoryCell = tr.ui.analysis.MemoryCell; + const expandTableRowsRecursively = tr.ui.analysis.expandTableRowsRecursively; + const aggregateTableRowCells = tr.ui.analysis.aggregateTableRowCells; + const aggregateTableRowCellsRecursively = + tr.ui.analysis.aggregateTableRowCellsRecursively; + const Scalar = tr.b.Scalar; + const sizeInBytes_smallerIsBetter = + tr.b.Unit.byName.sizeInBytes_smallerIsBetter; + const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields; + const checkNumericFields = tr.ui.analysis.checkNumericFields; + const checkStringFields = tr.ui.analysis.checkStringFields; + const createCells = tr.ui.analysis.createCells; + const createWarningInfo = tr.ui.analysis.createWarningInfo; + + function checkPercent(string, expectedPercent) { + assert.strictEqual(Number(string.slice(0, -1)), expectedPercent); + assert.strictEqual(string.slice(-1), '%'); + } + + function checkMemoryColumnFieldFormat(test, column, fields, + expectedTextContent, opt_expectedColor) { + const value = column.formatMultipleFields(fields); + if (expectedTextContent === undefined) { + assert.strictEqual(value, ''); + assert.isUndefined(opt_expectedColor); // Test sanity check. + return; + } + + const node = tr.ui.b.asHTMLOrTextNode(value); + const spanEl = document.createElement('span'); + Polymer.dom(spanEl).appendChild(node); + test.addHTMLOutput(spanEl); + + assert.strictEqual(Polymer.dom(node).textContent, expectedTextContent); + if (opt_expectedColor === undefined) { + assert.notInstanceOf(node, HTMLElement); + } else { + assert.strictEqual(node.style.color, opt_expectedColor); + } + } + + function checkCompareFieldsEqual(column, fieldValuesA, fieldValuesB) { + assert.strictEqual(column.compareFields(fieldValuesA, fieldValuesB), 0); + } + + function checkCompareFieldsLess(column, fieldValuesA, fieldValuesB) { + assert.isBelow(column.compareFields(fieldValuesA, fieldValuesB), 0); + assert.isAbove(column.compareFields(fieldValuesB, fieldValuesA), 0); + } + + function checkNumericMemoryColumnFieldFormat(test, column, fieldValues, unit, + expectedValue) { + const value = column.formatMultipleFields( + buildScalarCell(unit, fieldValues).fields); + if (expectedValue === undefined) { + assert.strictEqual(value, ''); + assert.isUndefined(expectedUnits); // Test sanity check. + return; + } + + test.addHTMLOutput(value); + assert.strictEqual(value.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(value.value, expectedValue); + assert.strictEqual(value.unit, unit); + } + + function buildScalarCell(unit, values) { + return new MemoryCell(values.map(function(value) { + if (value === undefined) return undefined; + return new Scalar(unit, value); + })); + } + + function buildTestRows() { + return [ + { + title: 'Row 1', + fields: { + 'cpu_temperature': new MemoryCell(['below zero', 'absolute zero']) + }, + subRows: [ + { + title: 'Row 1A', + fields: { + 'page_size': buildScalarCell(sizeInBytes_smallerIsBetter, + [1024, 1025]) + } + }, + { + title: 'Row 1B', + fields: { + 'page_size': buildScalarCell(sizeInBytes_smallerIsBetter, + [512, 513]), + 'mixed': new MemoryCell(['0.01', '0.10']), + 'mixed2': new MemoryCell([ + new Scalar(tr.b.Unit.byName.powerInWatts, 2.43e18), + new Scalar(tr.b.Unit.byName.powerInWatts, 0.5433) + ]) + } + } + ] + }, + { + title: 'Row 2', + fields: { + 'cpu_temperature': undefined, + 'mixed': buildScalarCell(tr.b.Unit.byName.timeDurationInMs, + [0.99, 0.999]) + } + } + ]; + } + + function checkCellValue( + test, value, expectedText, expectedColor, opt_expectedInfos) { + const expectedInfos = opt_expectedInfos || []; + assert.lengthOf(Polymer.dom(value).childNodes, 1 + expectedInfos.length); + assert.strictEqual(value.style.color, expectedColor); + if (typeof expectedText === 'string') { + assert.strictEqual( + Polymer.dom(Polymer.dom(value).childNodes[0]).textContent, + expectedText); + } else { + expectedText(Polymer.dom(value).childNodes[0]); + } + for (let i = 0; i < expectedInfos.length; i++) { + const expectedInfo = expectedInfos[i]; + const infoEl = Polymer.dom(value).childNodes[i + 1]; + assert.strictEqual(Polymer.dom(infoEl).textContent, expectedInfo.icon); + assert.strictEqual(infoEl.title, expectedInfo.message); + assert.strictEqual(infoEl.style.color, expectedInfo.color || ''); + } + test.addHTMLOutput(value); + } + + function sizeSpanMatcher( + expectedValue, opt_expectedIsDelta, opt_expectedContext) { + return function(element) { + assert.strictEqual(element.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(element.value, expectedValue); + assert.strictEqual(element.unit, opt_expectedIsDelta ? + tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter : + tr.b.Unit.byName.sizeInBytes_smallerIsBetter); + assert.deepEqual(element.context, opt_expectedContext); + }; + } + + test('checkTitleColumn_value', function() { + const column = new TitleColumn('column_title'); + assert.strictEqual(column.title, 'column_title'); + assert.isFalse(column.supportsCellSelection); + + let row = {title: 'undefined', contexts: undefined}; + assert.strictEqual(column.formatTitle(row), 'undefined'); + assert.strictEqual(column.value(row), 'undefined'); + + row = {title: 'constant', contexts: [{}, {}, {}, {}]}; + assert.strictEqual(column.formatTitle(row), 'constant'); + assert.strictEqual(column.value(row), 'constant'); + + row = {title: 'added', contexts: [undefined, undefined, undefined, {}]}; + assert.strictEqual(column.formatTitle(row), 'added'); + let value = column.value(row); + assert.strictEqual(Polymer.dom(value).textContent, '+++\u00A0added'); + assert.strictEqual(value.style.color, 'red'); + + row = {title: 'removed', contexts: [true, true, undefined, undefined]}; + assert.strictEqual(column.formatTitle(row), 'removed'); + value = column.value(row); + assert.strictEqual(Polymer.dom(value).textContent, '---\u00A0removed'); + assert.strictEqual(value.style.color, 'green'); + + row = {title: 'flaky', contexts: [true, undefined, true, true]}; + assert.strictEqual(column.formatTitle(row), 'flaky'); + value = column.value(row); + assert.strictEqual(Polymer.dom(value).textContent, 'flaky'); + assert.strictEqual(value.style.color, 'purple'); + + row = {title: 'added-flaky', contexts: [undefined, {}, undefined, true]}; + assert.strictEqual(column.formatTitle(row), 'added-flaky'); + value = column.value(row); + assert.strictEqual(Polymer.dom(value).textContent, '+++\u00A0added-flaky'); + assert.strictEqual(value.style.color, 'purple'); + + row = {title: 'removed-flaky', contexts: [true, undefined, {}, undefined]}; + assert.strictEqual(column.formatTitle(row), 'removed-flaky'); + value = column.value(row); + assert.strictEqual( + Polymer.dom(value).textContent, '---\u00A0removed-flaky'); + assert.strictEqual(value.style.color, 'purple'); + }); + + test('checkTitleColumn_cmp', function() { + const column = new TitleColumn('column_title'); + + assert.isBelow(column.cmp({title: 'a'}, {title: 'b'}), 0); + assert.strictEqual(column.cmp({title: 'cc'}, {title: 'cc'}), 0); + assert.isAbove(column.cmp({title: '10'}, {title: '2'}), 0); + }); + + test('checkMemoryColumn_fromRows', function() { + function MockColumn0() { + MemoryColumn.apply(this, arguments); + } + MockColumn0.prototype = { + __proto__: MemoryColumn.prototype, + get title() { return 'MockColumn0'; } + }; + + function MockColumn1() { + MemoryColumn.apply(this, arguments); + } + MockColumn1.prototype = { + __proto__: MemoryColumn.prototype, + get title() { return 'MockColumn1'; } + }; + + function MockColumn2() { + MemoryColumn.apply(this, arguments); + } + MockColumn2.prototype = { + __proto__: MemoryColumn.prototype, + get title() { return 'MockColumn2'; } + }; + + const rules = [ + { + condition: /size/, + importance: 10, + columnConstructor: MockColumn0 + }, + { + condition: 'cpu_temperature', + importance: 0, + columnConstructor: MockColumn1 + }, + { + condition: 'unmatched', + importance: -1, + get columnConstructor() { + throw new Error('The constructor should never be retrieved'); + } + }, + { + importance: 1, + columnConstructor: MockColumn2 + } + ]; + + const rows = buildTestRows(); + const columns = MemoryColumn.fromRows(rows, { + cellKey: 'fields', + aggregationMode: AggregationMode.MAX, + rules, + shouldSetContextGroup: true + }); + assert.lengthOf(columns, 4); + + const pageSizeColumn = columns[0]; + assert.strictEqual(pageSizeColumn.name, 'page_size'); + assert.strictEqual(pageSizeColumn.title, 'MockColumn0'); + assert.strictEqual(pageSizeColumn.aggregationMode, AggregationMode.MAX); + assert.strictEqual(pageSizeColumn.cell({fields: {page_size: 'large'}}), + 'large'); + assert.isTrue(pageSizeColumn.shouldSetContextGroup); + assert.instanceOf(pageSizeColumn, MockColumn0); + + const mixedColumn = columns[1]; + assert.strictEqual(mixedColumn.name, 'mixed'); + assert.strictEqual(mixedColumn.title, 'MockColumn2'); + assert.strictEqual(mixedColumn.aggregationMode, AggregationMode.MAX); + assert.strictEqual(mixedColumn.cell({fields: {mixed: 89}}), 89); + assert.isTrue(mixedColumn.shouldSetContextGroup); + assert.instanceOf(mixedColumn, MockColumn2); + + const mixed2Column = columns[2]; + assert.strictEqual(mixed2Column.name, 'mixed2'); + assert.strictEqual(mixed2Column.title, 'MockColumn2'); + assert.strictEqual(mixed2Column.aggregationMode, AggregationMode.MAX); + assert.strictEqual(mixed2Column.cell({fields: {mixed2: 'invalid'}}), + 'invalid'); + assert.isTrue(mixed2Column.shouldSetContextGroup); + assert.instanceOf(mixed2Column, MockColumn2); + + const cpuTemperatureColumn = columns[3]; + assert.strictEqual(cpuTemperatureColumn.name, 'cpu_temperature'); + assert.strictEqual(cpuTemperatureColumn.title, 'MockColumn1'); + assert.strictEqual(cpuTemperatureColumn.aggregationMode, + AggregationMode.MAX); + assert.strictEqual( + cpuTemperatureColumn.cell({fields: {cpu_temperature: 42}}), 42); + assert.isTrue(cpuTemperatureColumn.shouldSetContextGroup); + assert.instanceOf(cpuTemperatureColumn, MockColumn1); + }); + + test('checkMemoryColumn_spaceEqually', function() { + // Zero columns. + let columns = []; + MemoryColumn.spaceEqually(columns); + + // One column. + columns = [ + { + title: 'First Column', + value(row) { return row.firstData; } + } + ]; + MemoryColumn.spaceEqually(columns); + checkPercent(columns[0].width, 100); + + // Two columns. + columns = [ + { + title: 'First Column', + value(row) { return row.firstData; } + }, + { + title: 'Second Column', + value(row) { return row.firstData; } + } + ]; + MemoryColumn.spaceEqually(columns); + checkPercent(columns[0].width, 50); + checkPercent(columns[1].width, 50); + }); + + test('checkMemoryColumn_instantiate', function() { + const c = new MemoryColumn('test_column', ['x'], AggregationMode.MAX); + assert.strictEqual(c.name, 'test_column'); + assert.strictEqual(c.title, 'test_column'); + assert.strictEqual(c.cell({x: 95}), 95); + assert.isUndefined(c.width); + assert.isUndefined(c.color()); + }); + + test('checkMemoryColumn_cell', function() { + const c = new MemoryColumn('test_column', ['a', 'b'], AggregationMode.MAX); + const cell = new MemoryCell(undefined); + + assert.isUndefined(c.cell(undefined)); + assert.isUndefined(c.cell({b: cell})); + assert.isUndefined(c.cell({a: {c: cell}})); + assert.strictEqual(c.cell({a: {b: cell, c: 42}}), cell); + }); + + test('checkMemoryColumn_fields', function() { + const c = new MemoryColumn('test_column', ['x'], + AggregationMode.MAX); + + // Undefined cell or field inside cell. + assert.isUndefined(c.fields({})); + assert.isUndefined(c.fields({x: new MemoryCell(undefined)})); + + // Defined field(s) inside cell. + const field1 = new Scalar(tr.b.Unit.byName.powerInWatts, 1013.25); + const field2 = new Scalar(tr.b.Unit.byName.powerInWatts, 1065); + const row1 = {x: new MemoryCell([field1])}; + const row2 = {x: new MemoryCell([field1, field2])}; + assert.deepEqual(c.fields(row1), [field1]); + assert.deepEqual(c.fields(row2), [field1, field2]); + }); + + test('checkMemoryColumn_hasAllRelevantFieldsUndefined', function() { + // Single field. + const c1 = new MemoryColumn('single_column', ['x'], + undefined /* aggregation mode */); + assert.isTrue(c1.hasAllRelevantFieldsUndefined([undefined])); + assert.isFalse(c1.hasAllRelevantFieldsUndefined( + [new Scalar(sizeInBytes_smallerIsBetter, 16)])); + + // Multiple fields, diff aggregation mode. + const c2 = new MemoryColumn('diff_column', ['x'], + AggregationMode.DIFF); + assert.isTrue(c2.hasAllRelevantFieldsUndefined([undefined, undefined])); + assert.isTrue(c2.hasAllRelevantFieldsUndefined( + [undefined, undefined, undefined])); + assert.isTrue(c2.hasAllRelevantFieldsUndefined( + [undefined, new Scalar(sizeInBytes_smallerIsBetter, 16), undefined])); + assert.isFalse(c2.hasAllRelevantFieldsUndefined( + [undefined, new Scalar(sizeInBytes_smallerIsBetter, 32)])); + assert.isFalse(c2.hasAllRelevantFieldsUndefined( + [new Scalar(sizeInBytes_smallerIsBetter, 32), undefined, undefined])); + assert.isFalse(c2.hasAllRelevantFieldsUndefined([ + new Scalar(sizeInBytes_smallerIsBetter, 16), + undefined, + new Scalar(sizeInBytes_smallerIsBetter, 32) + ])); + + // Multiple fields, max aggregation mode. + const c3 = new MemoryColumn('max_column', ['x'], + AggregationMode.MAX); + assert.isTrue(c3.hasAllRelevantFieldsUndefined([undefined, undefined])); + assert.isTrue(c3.hasAllRelevantFieldsUndefined( + [undefined, undefined, undefined])); + assert.isFalse(c3.hasAllRelevantFieldsUndefined( + [undefined, new Scalar(sizeInBytes_smallerIsBetter, 16), undefined])); + assert.isFalse(c3.hasAllRelevantFieldsUndefined( + [undefined, new Scalar(sizeInBytes_smallerIsBetter, 32)])); + assert.isFalse(c3.hasAllRelevantFieldsUndefined([ + new Scalar(sizeInBytes_smallerIsBetter, 32), + undefined, + new Scalar(sizeInBytes_smallerIsBetter, 16) + ])); + }); + + test('checkMemoryColumn_value_allFieldsUndefined', function() { + const c1 = new MemoryColumn('no_color', ['x'], + AggregationMode.MAX); + const c2 = new MemoryColumn('color', ['x'], + AggregationMode.DIFF); + Object.defineProperty(c2, 'color', { + get() { + throw new Error('The color should never be retrieved'); + } + }); + + // Infos should be completely ignored. + c1.addInfos = c2.addInfos = function() { + throw new Error('This method should never be called'); + }; + + [c1, c2].forEach(function(c) { + assert.strictEqual(c.value({}), ''); + assert.strictEqual(c.value({x: new MemoryCell(undefined)}), ''); + assert.strictEqual(c.value({x: new MemoryCell([undefined])}), ''); + assert.strictEqual( + c.value({x: new MemoryCell([undefined, undefined])}), ''); + }); + + // Diff should only take into account the first and last field value. + assert.strictEqual(c2.value({ + x: new MemoryCell([ + undefined, + new Scalar(sizeInBytes_smallerIsBetter, 16), + undefined + ]) + }), ''); + }); + + test('checkMemoryColumn_getImportance', function() { + const c = new NumericMemoryColumn('test_column', ['x']); + + const rules1 = []; + assert.strictEqual(c.getImportance(rules1), 0); + + const rules2 = [ + { + condition: 'test', + importance: 4 + }, + { + condition: /test$/, + importance: 2 + } + ]; + assert.strictEqual(c.getImportance(rules2), 1); + + const rules3 = [ + { + condition: 'test_column', + importance: 10 + }, + { + importance: 5 + } + ]; + assert.strictEqual(c.getImportance(rules3), 10); + + const rules4 = [ + { + condition: 'test_column2', + importance: 8 + }, + { + condition: /column/, + importance: 12 + } + ]; + assert.strictEqual(c.getImportance(rules4), 12); + }); + + test('checkMemoryColumn_nameMatchesCondition', function() { + const c = new NumericMemoryColumn('test_column', ['x']); + + assert.isTrue(MemoryColumn.nameMatchesCondition('test_column', undefined)); + + assert.isFalse(MemoryColumn.nameMatchesCondition('test_column', 'test')); + assert.isTrue( + MemoryColumn.nameMatchesCondition('test_column', 'test_column')); + assert.isFalse( + MemoryColumn.nameMatchesCondition('test_column', 'test_column2')); + + assert.isTrue(MemoryColumn.nameMatchesCondition('test_column', /test/)); + assert.isTrue( + MemoryColumn.nameMatchesCondition('test_column', /^[^_]*_[^_]*$/)); + assert.isFalse(MemoryColumn.nameMatchesCondition('test_column', /test$/)); + }); + + test('checkStringMemoryColumn_value_singleField', function() { + const c = new StringMemoryColumn('', ['x'], AggregationMode.MAX); + c.color = function(fields, contexts) { + if (fields[0] < '0') return 'green'; + if (contexts && contexts[0] % 2 === 0) return 'red'; + return undefined; + }; + + const infos1 = [{ icon: '\u{1F648}', message: 'Some info', color: 'blue' }]; + const infos2 = [ + { icon: '\u{1F649}', message: 'Start', color: 'cyan' }, + { icon: '\u{1F64A}', message: 'Stop' } + ]; + c.addInfos = function(fields, contexts, infos) { + if (fields[0] < '0') { + infos.push.apply(infos, infos1); + } else if (contexts && contexts[0] % 2 === 0) { + infos.push.apply(infos, infos2); + } + }; + + let row = {x: new MemoryCell(['123'])}; + assert.strictEqual(c.value(row), '123'); + + row = {x: new MemoryCell(['-123']), contexts: [undefined]}; + checkCellValue(this, c.value(row), '-123', 'green', infos1); + + row = {x: new MemoryCell(['123']), contexts: [42]}; + checkCellValue(this, c.value(row), '123', 'red', infos2); + }); + + test('checkStringMemoryColumn_value_multipleFields', function() { + const c1 = new StringMemoryColumn('test_column1', ['x'], + undefined /* aggregation mode */); + const c2 = new StringMemoryColumn('test_column2', ['x'], + AggregationMode.DIFF); + c2.color = function(fields, contexts) { + return '#009999'; + }; + const c3 = new StringMemoryColumn('test_column3', ['x'], + AggregationMode.MAX); + c3.color = function(fields, contexts) { + if (fields[0] < '0') { + return 'green'; + } else if (contexts && contexts[contexts.length - 1] % 2 === 0) { + return 'red'; + } + return undefined; + }; + + const infos1 = [{ icon: '\u{1F648}', message: 'Some info', color: 'blue' }]; + const infos2 = [ + { icon: '\u{1F649}', message: 'Start', color: 'cyan' }, + { icon: '\u{1F64A}', message: 'Stop' } + ]; + c1.addInfos = c2.addInfos = c3.addInfos = + function(fields, contexts, infos) { + if (fields[0] < '0') { + infos.push.apply(infos, infos1); + } else if (contexts && contexts[contexts.length - 1] % 2 === 0) { + infos.push.apply(infos, infos2); + } + }; + + let row = {x: new MemoryCell(['123', '456'])}; + checkCellValue(this, c1.value(row), '(unsupported aggregation mode)', ''); + checkCellValue(this, c2.value(row), '123 \u2192 456', 'rgb(0, 153, 153)'); + assert.strictEqual(c3.value(row), '456'); + + row = { + x: new MemoryCell(['-123', undefined, '+123']), + contexts: [12, 14, undefined] + }; + checkCellValue(this, c1.value(row), '(unsupported aggregation mode)', '', + infos1); + checkCellValue(this, c2.value(row), '-123 \u2192 +123', 'rgb(0, 153, 153)', + infos1); + checkCellValue(this, c3.value(row), '+123', 'green', infos1); + + row = { + x: new MemoryCell(['123', undefined, '456']), + contexts: [31, 7, -2] + }; + checkCellValue(this, c1.value(row), '(unsupported aggregation mode)', '', + infos2); + checkCellValue(this, c2.value(row), '123 \u2192 456', 'rgb(0, 153, 153)', + infos2); + checkCellValue(this, c3.value(row), '456', 'red', infos2); + }); + + test('checkStringMemoryColumn_formatSingleField', function() { + const c = new StringMemoryColumn('test_column', ['x'], + undefined /* aggregation mode */); + + assert.strictEqual(c.formatSingleField('1024'), '1024'); + assert.strictEqual(c.formatSingleField('~10'), '~10'); + }); + + test('checkStringMemoryColumn_formatMultipleFields_diff', function() { + const c = new StringMemoryColumn('test_column', ['x'], + AggregationMode.DIFF); + + // Added value. + checkMemoryColumnFieldFormat(this, c, [undefined, 'few'], '+few', 'red'); + checkMemoryColumnFieldFormat(this, c, [undefined, 64, 32], '+32', 'red'); + + // Removed value. + checkMemoryColumnFieldFormat(this, c, ['00', undefined], '-00', 'green'); + checkMemoryColumnFieldFormat(this, c, [1, undefined, 2, undefined], '-1', + 'green'); + + // Identical values. + checkMemoryColumnFieldFormat(this, c, ['Unchanged', 'Unchanged'], + 'Unchanged', undefined /* unchanged color (not an HTML element) */); + checkMemoryColumnFieldFormat(this, c, [16, 32, undefined, 64, 16], '16', + undefined /* unchanged color (not an HTML element) */); + + // Different values. + checkMemoryColumnFieldFormat(this, c, ['A', 'C', undefined, 'C', 'B'], + 'A \u2192 B', 'darkorange'); + checkMemoryColumnFieldFormat(this, c, [16, undefined, 64], '16 \u2192 64', + 'darkorange'); + }); + + test('checkStringMemoryColumn_formatMultipleFields_max', function() { + const c = new StringMemoryColumn('test_column', ['x'], + AggregationMode.MAX); + + // Different values. + checkMemoryColumnFieldFormat(this, c, ['A', 'B', 'A'], 'B', + undefined /* unchanged color (not an HTML element) */); + checkMemoryColumnFieldFormat(this, c, [16, 16, undefined, 17], '17', + undefined /* unchanged color (not an HTML element) */); + + // Identical values. + checkMemoryColumnFieldFormat(this, c, ['X', 'X'], 'X', + undefined /* unchanged color (not an HTML element) */); + checkMemoryColumnFieldFormat(this, c, [7, undefined, 7, undefined, 7], '7', + undefined /* unchanged color (not an HTML element) */); + }); + + test('checkStringMemoryColumn_compareSingleFields', function() { + const c = new StringMemoryColumn('test_column', ['x'], + undefined /* aggregation mode */); + + assert.isBelow(c.compareSingleFields( + new Scalar(sizeInBytes_smallerIsBetter, 2), + new Scalar(sizeInBytes_smallerIsBetter, 10)), 0); + assert.strictEqual(c.compareSingleFields('equal', 'equal'), 0); + assert.isAbove(c.compareSingleFields('100', '99'), 0); + }); + + test('checkStringMemoryColumn_compareMultipleFields_diff', function() { + const c = new StringMemoryColumn('test_column', ['x'], + AggregationMode.DIFF); + + // One field was added. + checkCompareFieldsLess(c, [-10, 10], [undefined, 5]); + checkCompareFieldsLess(c, + [-100, undefined, undefined], [undefined, 4, 5]); + checkCompareFieldsLess(c, + [1, 2, 3, 4], [undefined, 'x', undefined, 'y']); + + // Both fields were added. + checkCompareFieldsEqual(c, + [undefined, 'C', undefined, 'A'], [undefined, 'B', 'D', 'A']); + checkCompareFieldsLess(c, [undefined, 1], [undefined, 2]); + checkCompareFieldsLess(c, [undefined, 6, 3], [undefined, 5, 4]); + + // One field was removed (neither was added). + checkCompareFieldsLess(c, ['B', undefined], ['A', 'A']); + checkCompareFieldsLess(c, + [5, undefined, undefined], [undefined, -5, -10]); + + // Both fields were removed (neither was added) + checkCompareFieldsEqual(c, ['T', 'A', undefined, undefined], + ['T', 'B', 'C', undefined]); + checkCompareFieldsLess(c, [5, undefined], [4, undefined]); + + // Neither field was added or removed. + checkCompareFieldsLess(c, ['BB', 'BB'], ['AA', 'CC']); + checkCompareFieldsEqual(c, [7, 8, 9], [6, 9, 10]); + checkCompareFieldsEqual(c, [5, undefined, 5], [4, 3, 4]); + }); + + test('checkStringMemoryColumn_compareMultipleFields_max', function() { + const c = new StringMemoryColumn('test_column', ['x'], + AggregationMode.MAX); + + // At least one field has multiple values. + checkCompareFieldsEqual(c, [0, 1, 3], [1, 3, 2]); + checkCompareFieldsLess(c, ['4', undefined, '4'], ['3', '4', '5']); + checkCompareFieldsLess(c, [3, 3, 3], [9, undefined, 10]); + + // Both fields have single values. + checkCompareFieldsEqual(c, + [undefined, 'ttt', undefined], ['ttt', 'ttt', undefined]); + checkCompareFieldsLess(c, [undefined, -1, undefined], [-2, -2, -2]); + checkCompareFieldsLess(c, ['Q', 'Q', undefined], ['X', undefined, 'X']); + }); + + test('checkStringMemoryColumn_cmp', function() { + const c = new StringMemoryColumn('test_column', ['x'], + AggregationMode.DIFF); + + // Cell (or the associated field) undefined in one or both rows. + assert.strictEqual(c.cmp({}, {y: new MemoryCell([undefined])}), 0); + assert.strictEqual(c.cmp({x: new MemoryCell(undefined)}, {}), 0); + assert.strictEqual( + c.cmp({x: new MemoryCell([undefined, undefined])}, {}), 0); + assert.isAbove(c.cmp({x: new MemoryCell(['negative'])}, {}), 0); + assert.isAbove(c.cmp({x: new MemoryCell(['negative'])}, + {x: new MemoryCell([undefined])}), 0); + assert.isBelow(c.cmp({}, {x: new MemoryCell(['positive'])}), 0); + assert.isBelow(c.cmp({x: new MemoryCell(undefined)}, + {x: new MemoryCell(['positive'])}), 0); + + // Single field. + assert.strictEqual(c.cmp({x: new MemoryCell(['equal'])}, + {x: new MemoryCell(['equal'])}), 0); + assert.isAbove(c.cmp({x: new MemoryCell(['bigger'])}, + {x: new MemoryCell(['BIG'])}), 0); + assert.isBelow(c.cmp({x: new MemoryCell(['small'])}, + {x: new MemoryCell(['smaLL'])}), 0); + + // Multiple fields. + assert.isBelow(c.cmp( + {x: new MemoryCell(['MemoryColumn', 'supports*', undefined])}, + {x: new MemoryCell(['comparing', 'multiple', 'values :-)'])}), 0); + }); + + test('checkNumericMemoryColumn_value', function() { + const c = new NumericMemoryColumn('test_column', ['x'], + AggregationMode.DIFF); + c.color = function(fields, contexts) { + return '#009999'; + }; + const infos1 = [createWarningInfo('Attention!')]; + c.addInfos = function(fields, contexts, infos) { + infos.push.apply(infos, infos1); + }; + + // Undefined field values. + let row = {x: buildScalarCell(sizeInBytes_smallerIsBetter, + [undefined, 1, undefined])}; + assert.strictEqual(c.value(row), ''); + + // Single field value. + row = {x: buildScalarCell(sizeInBytes_smallerIsBetter, + [5.4975581e13/* 50 TiB */])}; + checkCellValue(this, c.value(row), sizeSpanMatcher(5.4975581e13), + 'rgb(0, 153, 153)', infos1); + + // Multiple field values. + row = { + x: buildScalarCell(sizeInBytes_smallerIsBetter, + [5.4975581e13/* 50 TiB */, undefined, 2.1990233e13/* 20 TiB */]) + }; + checkCellValue(this, c.value(row), + sizeSpanMatcher(-3.2985348e13, true /* opt_expectedIsDelta */), + 'rgb(0, 153, 153)', infos1); + + // With custom formatting context. + c.getFormattingContext = function(unit) { + assert.strictEqual(unit, + tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter); + return { minimumFractionDigits: 2 }; + }; + checkCellValue(this, c.value(row), + sizeSpanMatcher(-3.2985348e13, true /* opt_expectedIsDelta */, + { minimumFractionDigits: 2 }), + 'rgb(0, 153, 153)', infos1); + }); + + test('checkNumericMemoryColumn_formatSingleField', function() { + let c = new NumericMemoryColumn('non_bytes_column', ['x'], + undefined /* aggregation mode */); + let value = c.formatSingleField(new Scalar( + tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 123)); + assert.strictEqual(value.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(value.value, 123); + assert.strictEqual(value.unit, + tr.b.Unit.byName.unitlessNumber_smallerIsBetter); + assert.isUndefined(value.contextGroup); + this.addHTMLOutput(value); + + c = new NumericMemoryColumn('bytes_column', ['x'], + undefined /* aggregation mode */); + c.shouldSetContextGroup = true; + value = c.formatSingleField(new Scalar( + sizeInBytes_smallerIsBetter, 456)); + assert.strictEqual(value.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(value.value, 456); + assert.strictEqual(value.unit, + tr.b.Unit.byName.sizeInBytes_smallerIsBetter); + assert.strictEqual(value.contextGroup, 'bytes_column'); + this.addHTMLOutput(value); + }); + + test('checkNumericMemoryColumn_formatMultipleFields_diff', + function() { + let c = new NumericMemoryColumn( + 'non_bytes_column', ['x'], AggregationMode.DIFF); + checkNumericMemoryColumnFieldFormat(this, c, [1, 2, 3], + tr.b.Unit.byName.unitlessNumberDelta_smallerIsBetter, 2); + checkNumericMemoryColumnFieldFormat(this, c, [10, undefined], + tr.b.Unit.byName.unitlessNumberDelta_smallerIsBetter, -10); + checkNumericMemoryColumnFieldFormat(this, c, [undefined, 60, 0], + tr.b.Unit.byName.unitlessNumberDelta_smallerIsBetter, 0); + checkNumericMemoryColumnFieldFormat( + this, c, [2.71828, 2.71829] /* diff within epsilon */, + tr.b.Unit.byName.unitlessNumberDelta_smallerIsBetter, 0); + + c = new NumericMemoryColumn( + 'bytes_column', ['x'], AggregationMode.DIFF); + checkNumericMemoryColumnFieldFormat(this, c, [1, 2, 3], + tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter, 2); + checkNumericMemoryColumnFieldFormat(this, c, [10, undefined], + tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter, -10); + checkNumericMemoryColumnFieldFormat(this, c, [undefined, 60, 0], + tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter, 0); + checkNumericMemoryColumnFieldFormat( + this, c, [1.41421, 1.41422] /* diff within epsilon */, + tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter, 0); + }); + + test('checkNumericMemoryColumn_formatMultipleFields_max', + function() { + let c = new NumericMemoryColumn( + 'non_bytes_column', ['x'], AggregationMode.MAX); + checkNumericMemoryColumnFieldFormat(this, c, [1, 2, 3], + tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 3); + checkNumericMemoryColumnFieldFormat(this, c, [10, undefined], + tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 10); + checkNumericMemoryColumnFieldFormat(this, c, [undefined, 60, 0], + tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 60); + checkNumericMemoryColumnFieldFormat( + this, c, [undefined, 10, 20, undefined], + tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 20); + + c = new NumericMemoryColumn( + 'bytes_column', ['x'], AggregationMode.MAX); + checkNumericMemoryColumnFieldFormat(this, c, [1, 2, 3], + tr.b.Unit.byName.sizeInBytes_smallerIsBetter, 3); + checkNumericMemoryColumnFieldFormat(this, c, [10, undefined], + tr.b.Unit.byName.sizeInBytes_smallerIsBetter, 10); + checkNumericMemoryColumnFieldFormat(this, c, [undefined, 60, 0], + tr.b.Unit.byName.sizeInBytes_smallerIsBetter, 60); + checkNumericMemoryColumnFieldFormat( + this, c, [undefined, 10, 20, undefined], + tr.b.Unit.byName.sizeInBytes_smallerIsBetter, 20); + }); + + test('checkNumericMemoryColumn_cmp', function() { + const c = new NumericMemoryColumn( + 'test_column', ['x'], AggregationMode.DIFF); + + // Undefined field values. + assert.isAbove(c.cmp({x: buildScalarCell(sizeInBytes_smallerIsBetter, + [-9999999999])}, + {x: undefined}), 0); + assert.isBelow(c.cmp({x: new MemoryCell(undefined)}, + {x: buildScalarCell(sizeInBytes_smallerIsBetter, [748, 749])}), 0); + assert.strictEqual( + c.cmp({}, { + x: buildScalarCell( + sizeInBytes_smallerIsBetter, [undefined, undefined]) + }), 0); + + // Single field value. + assert.isBelow(c.cmp( + {x: buildScalarCell(sizeInBytes_smallerIsBetter, [16384])}, + {x: buildScalarCell(sizeInBytes_smallerIsBetter, [32768])}), 0); + + // Multiple field values. + assert.strictEqual(c.cmp( + {x: buildScalarCell( + sizeInBytes_smallerIsBetter, [999, undefined, 1001])}, + {x: buildScalarCell( + sizeInBytes_smallerIsBetter, [undefined, 5, 2])}), 0); + }); + + test('checkNumericMemoryColumn_compareSingleFields', function() { + const c = new NumericMemoryColumn('test_column', ['x'], + undefined /* aggregation mode */); + + assert.isBelow(c.compareSingleFields( + new Scalar( + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, 99), + new Scalar( + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, 100)), 0); + assert.strictEqual(c.compareSingleFields( + new Scalar(tr.b.Unit.byName.unitlessNumber, 0xEEE), + new Scalar(tr.b.Unit.byName.unitlessNumber, 0xEEE)), 0); + assert.isAbove(c.compareSingleFields( + new Scalar(sizeInBytes_smallerIsBetter, 10), + new Scalar(sizeInBytes_smallerIsBetter, 2)), 0); + }); + + test('checkNumericMemoryColumn_compareMultipleFields_diff', function() { + const c = new NumericMemoryColumn('test_column', ['x'], + AggregationMode.DIFF); + + assert.isBelow(c.compareMultipleFields( + buildScalarCell(sizeInBytes_smallerIsBetter, + [10000, 10001, 10002] /* diff +2 */).fields, + buildScalarCell(sizeInBytes_smallerIsBetter, + [5, 7, 8] /* diff +3 */).fields), 0); + assert.strictEqual(c.compareMultipleFields( + buildScalarCell(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + [4, undefined] /* diff -4 */).fields, + buildScalarCell(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + [999, 995] /* diff -4 */).fields), 0); + assert.isAbove(c.compareMultipleFields( + buildScalarCell(sizeInBytes_smallerIsBetter, + [10, undefined, 12] /* diff +2 */).fields, + buildScalarCell(sizeInBytes_smallerIsBetter, + [11, 50, 12] /* diff +1 */).fields), 0); + assert.strictEqual(c.compareMultipleFields( + buildScalarCell(tr.b.Unit.byName.powerInWatts_smallerIsBetter, + [17, undefined, 17] /* diff 0 */).fields, + buildScalarCell(tr.b.Unit.byName.powerInWatts_smallerIsBetter, + [undefined, 100, undefined] /* diff 0 */).fields), 0); + assert.strictEqual(c.compareMultipleFields( + buildScalarCell(sizeInBytes_smallerIsBetter, + [3.14159, undefined, 3.14160] /* diff within epsilon */).fields, + buildScalarCell(sizeInBytes_smallerIsBetter, + [100, 100, 100] /* diff 0 */).fields), 0); + }); + + test('checkNumericMemoryColumn_compareMultipleFields_max', function() { + const c = new NumericMemoryColumn('test_column', ['x'], + AggregationMode.MAX); + + assert.isBelow(c.compareMultipleFields( + buildScalarCell(sizeInBytes_smallerIsBetter, + [10, undefined, 12]).fields, + buildScalarCell(sizeInBytes_smallerIsBetter, [11, 50, 12]).fields), 0); + assert.strictEqual(c.compareMultipleFields( + buildScalarCell(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + [999, undefined, -8888]).fields, + buildScalarCell(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + [undefined, 999, undefined]).fields), 0); + assert.isAbove(c.compareMultipleFields( + buildScalarCell(sizeInBytes_smallerIsBetter, + [10000, 10001, 10002]).fields, + buildScalarCell(sizeInBytes_smallerIsBetter, [5, 7, 8]).fields), 0); + assert.isBelow(c.compareMultipleFields( + buildScalarCell(tr.b.Unit.byName.powerInWatts_smallerIsBetter, + [17, undefined, 17]).fields, + buildScalarCell(tr.b.Unit.byName.powerInWatts_smallerIsBetter, + [undefined, 100, undefined]).fields), 0); + }); + + test('checkNumericMemoryColumn_getDiffFieldValue', function() { + const c = new NumericMemoryColumn('test_column', ['x'], + AggregationMode.MAX); + function checkDiffValue(first, last, expectedDiffValue) { + const actualDiffValue = c.getDiffFieldValue_( + first === undefined ? undefined : + new Scalar(sizeInBytes_smallerIsBetter, first), + last === undefined ? undefined : + new Scalar(sizeInBytes_smallerIsBetter, last)); + assert.closeTo(actualDiffValue, expectedDiffValue, 1e-8); + } + + // Diff outside epsilon range. + checkDiffValue(0, 0.0002, 0.0002); + checkDiffValue(undefined, 0.0003, 0.0003); + checkDiffValue(0.3334, 0.3332, -0.0002); + checkDiffValue(0.0005, undefined, -0.0005); + + // Diff inside epsilon range. + checkDiffValue(5, 5.00009, 0); + checkDiffValue(undefined, 0.0000888, 0); + checkDiffValue(0.29999, 0.3, 0); + checkDiffValue(0.00009, undefined, 0); + checkDiffValue(0.777777, 0.777777, 0); + checkDiffValue(undefined, undefined, 0); + }); + + test('checkExpandTableRowsRecursively', function() { + const columns = [ + { + title: 'Single column', + value(row) { return row.data; }, + width: '100px' + } + ]; + + const rows = [ + { + data: 'allocated', + subRows: [ + { + data: 'v8', + subRows: [] + }, + { + data: 'oilpan', + subRows: [ + { + data: 'still_visible', + subRows: [ + { + data: 'not_visible_any_more' + } + ] + }, + { + data: 'also_visible' + } + ] + } + ] + }, + { + data: 'no_sub_rows' + }, + { + data: 'fragmentation', + subRows: [ + { + data: 'internal' + }, + { + data: 'external', + subRows: [ + { + data: 'unexpanded' + } + ] + } + ] + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + + expandTableRowsRecursively(table); + + function isExpanded(row) { return table.getExpandedForTableRow(row); } + + // Level 0 (3 rows) should be expanded (except for nodes which have no + // sub-rows). + assert.isTrue(isExpanded(rows[0] /* allocated */)); + assert.isFalse(isExpanded(rows[1] /* no_sub_rows */)); + assert.isTrue(isExpanded(rows[2] /* overhead */)); + + // Level 1 (4 rows) should be expanded (except for nodes which have no + // sub-rows). + assert.isFalse(isExpanded(rows[0].subRows[0] /* allocated/v8 */)); + assert.isTrue(isExpanded(rows[0].subRows[1] /* allocated/oilpan */)); + assert.isFalse(isExpanded(rows[2].subRows[0] /* fragmentation/internal */)); + assert.isTrue(isExpanded(rows[2].subRows[1] /* fragmentation/external */)); + + // Level 2 (3 rows) should not be expanded any more. + assert.isFalse(isExpanded( + rows[0].subRows[1].subRows[0] /* allocated/oilpan/still_visible */)); + assert.isFalse(isExpanded( + rows[0].subRows[1].subRows[1] /* allocated/oilpan/also_visible */)); + assert.isFalse(isExpanded( + rows[2].subRows[1].subRows[0] /* fragmentation/external/unexpanded */)); + }); + + test('checkMemoryCell_extractFields', function() { + assert.isUndefined(MemoryCell.extractFields(undefined)); + + assert.isUndefined(MemoryCell.extractFields(new MemoryCell(undefined))); + + const fields = [new Scalar(sizeInBytes_smallerIsBetter, 1024)]; + assert.strictEqual( + MemoryCell.extractFields(new MemoryCell(fields)), fields); + }); + + test('checkAggregateTableRowCellsRecursively', function() { + const row = { + testCells: { + a: buildScalarCell(sizeInBytes_smallerIsBetter, [17]) + }, + subRows: [ + { + // Intentionally no testCells. + subRows: [ + { + testCells: { + b: buildScalarCell(sizeInBytes_smallerIsBetter, [103]), + c: new MemoryCell(['should-not-propagate-upwards']), + d: buildScalarCell(sizeInBytes_smallerIsBetter, [-200]) + } + // Intentionally no subRows. + }, + { + testCells: {}, + subRows: [] + } + ], + contexts: ['skip-row-when-using-predicate'] + }, + { + testCells: { + b: buildScalarCell(sizeInBytes_smallerIsBetter, [20]), + a: buildScalarCell(sizeInBytes_smallerIsBetter, [13]), + e: buildScalarCell(sizeInBytes_smallerIsBetter, [-300]) + }, + contexts: ['don\'t-skip'] + } + ] + }; + + // Without a predicate. + const ca = new NumericMemoryColumn('column_a', ['testCells', 'a']); + const cb = new NumericMemoryColumn('column_b', ['testCells', 'b']); + const cc = new StringMemoryColumn('column_c', ['testCells', 'c']); + aggregateTableRowCellsRecursively(row, [ca, cb, cc]); + checkSizeNumericFields(row, ca, [17]); + checkSizeNumericFields(row, cb, [123]); + checkStringFields(row, cc, undefined); + + // With a predicate. + const cd = new NumericMemoryColumn('column_d', ['testCells', 'd']); + const ce = new NumericMemoryColumn('column_e', ['testCells', 'e']); + aggregateTableRowCellsRecursively(row, [cd, ce], function(contexts) { + return contexts === undefined || !contexts[0].startsWith('skip'); + }); + checkSizeNumericFields(row, cd, undefined); + checkSizeNumericFields(row, ce, [-300]); + }); + + test('checkAggregateTableRowCells', function() { + const row = { + // Intentionally no testCells. + otherCells: { + a: buildScalarCell(tr.b.Unit.byName.unitlessNumber, + [5, undefined, undefined]) + } + }; + const subRows = [ + { + testCells: { + a: buildScalarCell(sizeInBytes_smallerIsBetter, [1, 9]) + }, + subRows: [ + { + testCells: { + c: buildScalarCell(sizeInBytes_smallerIsBetter, [13]) + } + } + ] + }, + { + testCells: { + a: buildScalarCell(sizeInBytes_smallerIsBetter, [2, 17]), + b: buildScalarCell(sizeInBytes_smallerIsBetter, [5]) + }, + otherCells: { + a: buildScalarCell(tr.b.Unit.byName.unitlessNumber, + [153, undefined, 257]), + b: new MemoryCell(['field-should-not-propagate-upwards', '']) + } + } + ]; + + const cta = new NumericMemoryColumn('column_test_a', ['testCells', 'a']); + const ctb = new NumericMemoryColumn('column_test_b', ['testCells', 'b']); + const ctc = new NumericMemoryColumn('column_test_c', ['testCells', 'c']); + const coa = new NumericMemoryColumn('column_other_a', ['otherCells', 'a']); + const cob = new StringMemoryColumn('column_other_b', ['otherCells', 'b']); + + aggregateTableRowCells(row, subRows, [cta, ctb, ctc, coa, cob]); + + checkSizeNumericFields(row, cta, [3, 26]); + checkSizeNumericFields(row, ctb, [5]); + checkSizeNumericFields(row, ctc, undefined); + + checkNumericFields(row, coa, [5, undefined, 257], + tr.b.Unit.byName.unitlessNumber); + checkStringFields(row, cob, undefined); + }); + + test('checkCreateCells', function() { + const values = [ + { + a: 9, + b: 314 + }, + { + b: 159, + c: undefined + }, + undefined, + { + b: 265, + d: 0 + } + ]; + + const mockColumn = new MemoryColumn('', [], undefined); + + const cells = createCells(values, function(dict) { + const fields = {}; + for (const [key, value] of Object.entries(dict)) { + if (value === undefined) continue; + fields[key] = new Scalar(sizeInBytes_smallerIsBetter, value); + } + return fields; + }); + assert.deepEqual(Object.keys(cells), ['a', 'b', 'd']); + checkSizeNumericFields( + cells.a, mockColumn, [9, undefined, undefined, undefined]); + checkSizeNumericFields(cells.b, mockColumn, [314, 159, undefined, 265]); + checkSizeNumericFields( + cells.d, mockColumn, [undefined, undefined, undefined, 0]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html new file mode 100644 index 00000000000..2a20bb3c27e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html @@ -0,0 +1,382 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> +<link rel="import" href="/tracing/ui/analysis/stacked_pane.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-a-memory-dump-vm-regions-details-pane'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + + #label { + flex: 0 0 auto; + padding: 8px; + + background-color: #eee; + border-bottom: 1px solid #8e8e8e; + border-top: 1px solid white; + + font-size: 15px; + font-weight: bold; + } + + #contents { + flex: 1 0 auto; + align-self: stretch; + font-size: 12px; + } + + #info_text { + padding: 8px; + color: #666; + font-style: italic; + text-align: center; + } + + #table { + display: none; /* Hide until memory dumps are set. */ + flex: 1 0 auto; + align-self: stretch; + font-size: 12px; + } + </style> + <div id="label">Memory maps</div> + <div id="contents"> + <div id="info_text">No memory maps selected</div> + <tr-ui-b-table id="table"></tr-ui-b-table> + </div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const Scalar = tr.b.Scalar; + const sizeInBytes_smallerIsBetter = + tr.b.Unit.byName.sizeInBytes_smallerIsBetter; + + const CONSTANT_COLUMN_RULES = [ + { + condition: 'Start address', + importance: 0, + columnConstructor: tr.ui.analysis.StringMemoryColumn + } + ]; + + const VARIABLE_COLUMN_RULES = [ + { + condition: 'Virtual size', + importance: 7, + columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn + }, + { + condition: 'Protection flags', + importance: 6, + columnConstructor: tr.ui.analysis.StringMemoryColumn + }, + { + condition: 'PSS', + importance: 5, + columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn + }, + { + condition: 'Private dirty', + importance: 4, + columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn + }, + { + condition: 'Private clean', + importance: 3, + columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn + }, + { + condition: 'Shared dirty', + importance: 2, + columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn + }, + { + condition: 'Shared clean', + importance: 1, + columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn + }, + { + condition: 'Swapped', + importance: 0, + columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn + } + ]; + + const BYTE_STAT_COLUMN_MAP = { + 'proportionalResident': 'PSS', + 'privateDirtyResident': 'Private dirty', + 'privateCleanResident': 'Private clean', + 'sharedDirtyResident': 'Shared dirty', + 'sharedCleanResident': 'Shared clean', + 'swapped': 'Swapped' + }; + + function hexString(address, is64BitAddress) { + if (address === undefined) return undefined; + const hexPadding = is64BitAddress ? '0000000000000000' : '00000000'; + return (hexPadding + address.toString(16)).substr(-hexPadding.length); + } + + function pruneEmptyRuleRows(row) { + if (row.subRows === undefined || row.subRows.length === 0) return; + + // Either all sub-rows are rule rows, or all sub-rows are VM region rows. + if (row.subRows[0].rule === undefined) { + // VM region rows: Early out to avoid filtering a large array for + // performance reasons (no sub-rows would be removed, but the whole array + // would be unnecessarily copied to a new array). + return; + } + + row.subRows.forEach(pruneEmptyRuleRows); + row.subRows = row.subRows.filter(function(subRow) { + return subRow.subRows.length > 0; + }); + } + + Polymer({ + is: 'tr-ui-a-memory-dump-vm-regions-details-pane', + behaviors: [tr.ui.analysis.StackedPane], + + created() { + this.vmRegions_ = undefined; + this.aggregationMode_ = undefined; + }, + + ready() { + this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + }, + + /** + * Sets the VM regions and schedules rebuilding the pane. + * + * The provided value should be a chronological list of lists of VM + * regions. All VM regions are assumed to belong to the same process. + * Example: + * + * [ + * [ + * // VM regions at timestamp 1. + * tr.model.VMRegion {}, + * tr.model.VMRegion {}, + * tr.model.VMRegion {} + * ], + * undefined, // No VM regions provided at timestamp 2. + * [ + * // VM regions at timestamp 3. + * tr.model.VMRegion {}, + * tr.model.VMRegion {} + * ] + * ] + */ + set vmRegions(vmRegions) { + this.vmRegions_ = vmRegions; + this.scheduleRebuild_(); + }, + + get vmRegions() { + return this.vmRegions_; + }, + + set aggregationMode(aggregationMode) { + this.aggregationMode_ = aggregationMode; + this.scheduleRebuild_(); + }, + + get aggregationMode() { + return this.aggregationMode_; + }, + + onRebuild_() { + if (this.vmRegions_ === undefined || this.vmRegions_.length === 0) { + // Show the info text (hide the table). + this.$.info_text.style.display = 'block'; + this.$.table.style.display = 'none'; + + this.$.table.clear(); + this.$.table.rebuild(); + return; + } + + // Show the table (hide the info text). + this.$.info_text.style.display = 'none'; + this.$.table.style.display = 'block'; + + const rows = this.createRows_(this.vmRegions_); + const columns = this.createColumns_(rows); + + // Note: There is no need to aggregate fields of the VM regions because + // the classification tree already takes care of that. + + this.$.table.tableRows = rows; + this.$.table.tableColumns = columns; + + // TODO(petrcermak): This can be quite slow. Consider doing this somehow + // asynchronously. + this.$.table.rebuild(); + + tr.ui.analysis.expandTableRowsRecursively(this.$.table); + }, + + createRows_(timeToVmRegionTree) { + // Determine if any start address is outside the 32-bit range. + const is64BitAddress = timeToVmRegionTree.some(function(vmRegionTree) { + if (vmRegionTree === undefined) return false; + return vmRegionTree.someRegion(function(region) { + if (region.startAddress === undefined) return false; + return region.startAddress >= 4294967296; /* 2^32 */ + }); + }); + + return [ + this.createClassificationNodeRow(timeToVmRegionTree, is64BitAddress) + ]; + }, + + createClassificationNodeRow(timeToNode, is64BitAddress) { + // Get any defined classification node so that we can extract the + // properties which don't change over time. + const definedNode = timeToNode.find(x => x); + + // Child node ID (list index) -> Timestamp (list index) -> + // VM region classification node. + const childNodeIdToTimeToNode = Object.values( + tr.b.invertArrayOfDicts(timeToNode, function(node) { + const children = node.children; + if (children === undefined) return undefined; + const childMap = {}; + children.forEach(function(childNode) { + if (!childNode.hasRegions) return; + childMap[childNode.title] = childNode; + }); + return childMap; + })); + const childNodeSubRows = childNodeIdToTimeToNode.map( + function(timeToChildNode) { + return this.createClassificationNodeRow( + timeToChildNode, is64BitAddress); + }, this); + + // Region ID (list index) -> Timestamp (list index) -> VM region. + const regionIdToTimeToRegion = Object.values( + tr.b.invertArrayOfDicts(timeToNode, function(node) { + const regions = node.regions; + if (regions === undefined) return undefined; + + const results = {}; + for (const region of regions) { + results[region.uniqueIdWithinProcess] = region; + } + return results; + })); + const regionSubRows = regionIdToTimeToRegion.map(function(timeToRegion) { + return this.createRegionRow_(timeToRegion, is64BitAddress); + }, this); + + const subRows = childNodeSubRows.concat(regionSubRows); + + return { + title: definedNode.title, + contexts: timeToNode, + variableCells: this.createVariableCells_(timeToNode), + subRows + }; + }, + + createRegionRow_(timeToRegion, is64BitAddress) { + // Get any defined VM region so that we can extract the properties which + // don't change over time. + const definedRegion = timeToRegion.find(x => x); + + return { + title: definedRegion.mappedFile, + contexts: timeToRegion, + constantCells: this.createConstantCells_(definedRegion, is64BitAddress), + variableCells: this.createVariableCells_(timeToRegion) + }; + }, + + /** + * Create cells for VM region properties which DON'T change over time. + * + * Note that there are currently no such properties of classification nodes. + */ + createConstantCells_(definedRegion, is64BitAddress) { + return tr.ui.analysis.createCells([definedRegion], function(region) { + const startAddress = region.startAddress; + if (startAddress === undefined) return undefined; + return { 'Start address': hexString(startAddress, is64BitAddress) }; + }); + }, + + /** + * Create cells for VM region (classification node) properties which DO + * change over time. + */ + createVariableCells_(timeToRegion) { + return tr.ui.analysis.createCells(timeToRegion, function(region) { + const fields = {}; + + const sizeInBytes = region.sizeInBytes; + if (sizeInBytes !== undefined) { + fields['Virtual size'] = new Scalar( + sizeInBytes_smallerIsBetter, sizeInBytes); + } + const protectionFlags = region.protectionFlagsToString; + if (protectionFlags !== undefined) { + fields['Protection flags'] = protectionFlags; + } + + for (const [byteStatName, columnName] of + Object.entries(BYTE_STAT_COLUMN_MAP)) { + const byteStat = region.byteStats[byteStatName]; + if (byteStat === undefined) continue; + fields[columnName] = new Scalar( + sizeInBytes_smallerIsBetter, byteStat); + } + + return fields; + }); + }, + + createColumns_(rows) { + const titleColumn = new tr.ui.analysis.TitleColumn('Mapped file'); + titleColumn.width = '200px'; + + const constantColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, { + cellKey: 'constantCells', + aggregationMode: undefined, + rules: CONSTANT_COLUMN_RULES + }); + const variableColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, { + cellKey: 'variableCells', + aggregationMode: this.aggregationMode_, + rules: VARIABLE_COLUMN_RULES + }); + const fieldColumns = constantColumns.concat(variableColumns); + tr.ui.analysis.MemoryColumn.spaceEqually(fieldColumns); + + const columns = [titleColumn].concat(fieldColumns); + return columns; + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane_test.html new file mode 100644 index 00000000000..7534727091b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane_test.html @@ -0,0 +1,496 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/container_memory_dump.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> +<link rel="import" href="/tracing/model/vm_region.html"> +<link rel="import" + href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html"> +<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html"> +<link rel="import" + href="/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump; + const VMRegion = tr.model.VMRegion; + const VMRegionClassificationNode = tr.model.VMRegionClassificationNode; + const TitleColumn = tr.ui.analysis.TitleColumn; + const StringMemoryColumn = tr.ui.analysis.StringMemoryColumn; + const NumericMemoryColumn = tr.ui.analysis.NumericMemoryColumn; + const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode; + const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump; + const addProcessMemoryDump = + tr.model.MemoryDumpTestUtils.addProcessMemoryDump; + const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields; + const checkStringFields = tr.ui.analysis.checkStringFields; + const checkColumns = tr.ui.analysis.checkColumns; + const isElementDisplayed = tr.ui.analysis.isElementDisplayed; + const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED; + + function createVMRegions() { + const model = tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + + // First timestamp. + const gmd1 = addGlobalMemoryDump( + model, {ts: 42, levelOfDetail: DETAILED}); + const pmd1 = addProcessMemoryDump(gmd1, process, {ts: 42}); + pmd1.vmRegions = VMRegionClassificationNode.fromRegions([ + VMRegion.fromDict({ + mappedFile: '/lib/chrome.so', + startAddress: 65536, + sizeInBytes: 536870912, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE, + byteStats: { + proportionalResident: 8192 + } + }), + VMRegion.fromDict({ + mappedFile: '/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0', + startAddress: 140296983150592, + sizeInBytes: 2097152, + protectionFlags: 0, + byteStats: { + proportionalResident: 0 + } + }), + VMRegion.fromDict({ + startAddress: 10995116277760, + sizeInBytes: 2147483648, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_WRITE, + byteStats: { + privateDirtyResident: 0, + swapped: 0 + } + }), + VMRegion.fromDict({ + startAddress: 12094627905536, + sizeInBytes: 2147483648, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_WRITE, + byteStats: { + privateDirtyResident: 0, + swapped: 0 + } + }), + VMRegion.fromDict({ + mappedFile: '/dev/ashmem/dalvik-zygote space', + startAddress: 13194139533312, + sizeInBytes: 100, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE, + byteStats: { + proportionalResident: 100, + privateDirtyResident: 0, + swapped: 0 + } + }), + VMRegion.fromDict({ + mappedFile: '/dev/ashmem/libc malloc', + startAddress: 14293651161088, + sizeInBytes: 200, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE, + byteStats: { + proportionalResident: 200, + privateDirtyResident: 96, + swapped: 0 + } + }) + ]); + + // This is here so that we could test that tracing is discounted from the + // 'Native heap' category. + pmd1.memoryAllocatorDumps = [ + newAllocatorDump(pmd1, 'tracing', + {numerics: {size: 500, resident_size: 32}}) + ]; + + // Second timestamp. + const gmd2 = addGlobalMemoryDump( + model, {ts: 42, levelOfDetail: DETAILED}); + const pmd2 = addProcessMemoryDump(gmd2, process, {ts: 42}); + pmd2.vmRegions = VMRegionClassificationNode.fromRegions([ + VMRegion.fromDict({ + mappedFile: '/lib/chrome.so', + startAddress: 65536, + sizeInBytes: 536870912, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE, + byteStats: { + proportionalResident: 9216 + } + }), + VMRegion.fromDict({ + mappedFile: '/lib/chrome.so', + startAddress: 140296983150592, + sizeInBytes: 536870912, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE, + byteStats: { + proportionalResident: 10240 + } + }), + VMRegion.fromDict({ + startAddress: 10995116277760, + sizeInBytes: 2147483648, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_WRITE, + byteStats: { + privateDirtyResident: 0, + swapped: 32 + } + }), + VMRegion.fromDict({ + startAddress: 12094627905536, + sizeInBytes: 2147483648, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_WRITE, + byteStats: { + privateDirtyResident: 0, + swapped: 0 + } + }), + VMRegion.fromDict({ + mappedFile: '/dev/ashmem/dalvik-zygote space', + startAddress: 13194139533312, + sizeInBytes: 100, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE, + byteStats: { + proportionalResident: 0, + privateDirtyResident: 100, + swapped: 0 + } + }), + VMRegion.fromDict({ + mappedFile: '/dev/ashmem/libc malloc', + startAddress: 14293651161088, + sizeInBytes: 200, + protectionFlags: VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE, + byteStats: { + proportionalResident: 100, + privateDirtyResident: 96, + swapped: 0 + } + }), + VMRegion.fromDict({ + mappedFile: '/usr/share/fonts/DejaVuSansMono.ttf', + startAddress: 140121259503616, + sizeInBytes: 335872, + protectionFlags: VMRegion.PROTECTION_FLAG_READ, + byteStats: { + proportionalResident: 22528 + } + }), + VMRegion.fromDict({ + mappedFile: 'another-map', + startAddress: 52583094233905872, + sizeInBytes: 1, + byteStats: { + proportionalResident: 1, + privateDirtyResident: 1, + swapped: 1 + } + }) + ]); + }); + + return model.processes[1].memoryDumps.map(function(pmd) { + return pmd.mostRecentVmRegions; + }); + } + + const EXPECTED_COLUMNS = [ + { title: 'Mapped file', type: TitleColumn, noAggregation: true }, + { title: 'Start address', type: StringMemoryColumn, noAggregation: true }, + { title: 'Virtual size', type: NumericMemoryColumn }, + { title: 'Protection flags', type: StringMemoryColumn }, + { title: 'PSS', type: NumericMemoryColumn }, + { title: 'Private dirty', type: NumericMemoryColumn }, + { title: 'Swapped', type: NumericMemoryColumn } + ]; + + function checkRow(columns, row, expectedTitle, expectedStartAddress, + expectedVirtualSize, expectedProtectionFlags, + expectedProportionalResidentValues, expectedPrivateDirtyResidentValues, + expectedSwappedValues, expectedSubRowCount, expectedContexts) { + assert.strictEqual(columns[0].formatTitle(row), expectedTitle); + checkStringFields(row, columns[1], expectedStartAddress); + checkSizeNumericFields(row, columns[2], expectedVirtualSize); + checkStringFields(row, columns[3], expectedProtectionFlags); + checkSizeNumericFields(row, columns[4], expectedProportionalResidentValues); + checkSizeNumericFields(row, columns[5], expectedPrivateDirtyResidentValues); + checkSizeNumericFields(row, columns[6], expectedSwappedValues); + + if (expectedSubRowCount === undefined) { + assert.isUndefined(row.subRows); + } else { + assert.lengthOf(row.subRows, expectedSubRowCount); + } + + if (typeof expectedContexts === 'function') { + expectedContexts(row.contexts); + } else if (expectedContexts !== undefined) { + assert.deepEqual(Array.from(row.contexts), expectedContexts); + } else { + assert.isUndefined(row.contexts); + } + } + + function genericMatcher(callback, defined) { + return function(actualValues) { + assert.lengthOf(actualValues, defined.length); + for (let i = 0; i < defined.length; i++) { + const actualValue = actualValues[i]; + if (defined[i]) { + callback(actualValue); + } else { + assert.isUndefined(actualValue); + } + } + }; + } + + function vmRegionsMatcher(expectedMappedFile, expectedStartAddress, defined) { + return genericMatcher(function(actualRegion) { + assert.instanceOf(actualRegion, VMRegion); + assert.strictEqual(actualRegion.mappedFile, expectedMappedFile); + assert.strictEqual(actualRegion.startAddress, expectedStartAddress); + }, defined); + } + + function classificationNodesMatcher(expectedTitle, defined) { + return genericMatcher(function(actualNode) { + assert.instanceOf(actualNode, VMRegionClassificationNode); + assert.strictEqual(actualNode.title, expectedTitle); + }, defined); + } + + test('instantiate_empty', function() { + tr.ui.analysis.createAndCheckEmptyPanes(this, + 'tr-ui-a-memory-dump-vm-regions-details-pane', 'vmRegions', + function(viewEl) { + // Check that the info text is shown. + assert.isTrue(isElementDisplayed(viewEl.$.info_text)); + assert.isFalse(isElementDisplayed(viewEl.$.table)); + }); + }); + + test('instantiate_single', function() { + const vmRegions = createVMRegions().slice(0, 1); + + const viewEl = document.createElement( + 'tr-ui-a-memory-dump-vm-regions-details-pane'); + viewEl.vmRegions = vmRegions; + viewEl.rebuild(); + this.addHTMLOutput(viewEl); + + // Check that the table is shown. + assert.isTrue(isElementDisplayed(viewEl.$.table)); + assert.isFalse(isElementDisplayed(viewEl.$.info_text)); + + const table = viewEl.$.table; + const columns = table.tableColumns; + checkColumns(columns, EXPECTED_COLUMNS, undefined /* no aggregation */); + const rows = table.tableRows; + assert.lengthOf(rows, 1); + + // Check the rows of the table. + const totalRow = rows[0]; + checkRow(columns, totalRow, 'Total', undefined, [4833935160], undefined, + [8460], [64], [0], 3, vmRegions); + + const androidRow = totalRow.subRows[0]; + checkRow(columns, androidRow, 'Android', undefined, [100], undefined, + [100], [0], [0], 1, classificationNodesMatcher('Android', [true])); + + const javaRuntimeRow = androidRow.subRows[0]; + checkRow(columns, javaRuntimeRow, 'Java runtime', undefined, [100], + undefined, [100], [0], [0], 1, + classificationNodesMatcher('Java runtime', [true])); + + const spacesRow = javaRuntimeRow.subRows[0]; + checkRow(columns, spacesRow, 'Spaces', undefined, [100], undefined, [100], + [0], [0], 1, classificationNodesMatcher('Spaces', [true])); + + const nativeHeapRow = totalRow.subRows[1]; + checkRow(columns, nativeHeapRow, 'Native heap', undefined, [4294966996], + undefined, [168], [64], [0], 4, + classificationNodesMatcher('Native heap', [true])); + + const discountedTracingOverheadRow = nativeHeapRow.subRows[3]; + checkRow(columns, discountedTracingOverheadRow, + '[discounted tracing overhead]', undefined, [-500], undefined, [-32], + [-32], undefined, undefined, + vmRegionsMatcher('[discounted tracing overhead]', undefined, [true])); + + const filesRow = totalRow.subRows[2]; + checkRow(columns, filesRow, 'Files', undefined, [538968064], undefined, + [8192], undefined, undefined, 1, + classificationNodesMatcher('Files', [true])); + + const soRow = filesRow.subRows[0]; + checkRow(columns, soRow, 'so', undefined, [538968064], undefined, + [8192], undefined, undefined, 2, + classificationNodesMatcher('so', [true])); + + const mmapChromeRow = soRow.subRows[0]; + checkRow(columns, mmapChromeRow, '/lib/chrome.so', ['0000000000010000'], + [536870912], ['r-xp'], [8192], undefined, undefined, undefined, + vmRegionsMatcher('/lib/chrome.so', 65536, [true])); + + const mmapLibX11Row = soRow.subRows[1]; + checkRow(columns, mmapLibX11Row, + '/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0', ['00007f996fd80000'], + [2097152], ['---p'], [0], undefined, undefined, undefined, + vmRegionsMatcher('/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0', + 140296983150592, [true])); + }); + + test('instantiate_multipleDiff', function() { + const vmRegions = createVMRegions(); + + const viewEl = document.createElement( + 'tr-ui-a-memory-dump-vm-regions-details-pane'); + viewEl.vmRegions = vmRegions; + viewEl.aggregationMode = AggregationMode.DIFF; + viewEl.rebuild(); + this.addHTMLOutput(viewEl); + + // Check that the table is shown. + assert.isTrue(isElementDisplayed(viewEl.$.table)); + assert.isFalse(isElementDisplayed(viewEl.$.info_text)); + + const table = viewEl.$.table; + const columns = table.tableColumns; + checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.DIFF); + const rows = table.tableRows; + assert.lengthOf(rows, 1); + + // Check the rows of the table. + const totalRow = rows[0]; + checkRow(columns, totalRow, 'Total', undefined, [4833935160, 5369045293], + undefined, [8460, 42085], [64, 197], [0, 33], 4, vmRegions); + + const androidRow = totalRow.subRows[0]; + checkRow(columns, androidRow, 'Android', undefined, [100, 100], undefined, + [100, 0], [0, 100], [0, 0], 1, + classificationNodesMatcher('Android', [true, true])); + + const javaRuntimeRow = androidRow.subRows[0]; + checkRow(columns, javaRuntimeRow, 'Java runtime', undefined, [100, 100], + undefined, [100, 0], [0, 100], [0, 0], 1, + classificationNodesMatcher('Java runtime', [true, true])); + + const spacesRow = javaRuntimeRow.subRows[0]; + checkRow(columns, spacesRow, 'Spaces', undefined, [100, 100], undefined, + [100, 0], [0, 100], [0, 0], 1, + classificationNodesMatcher('Spaces', [true, true])); + + const nativeHeapRow = totalRow.subRows[1]; + checkRow(columns, nativeHeapRow, 'Native heap', undefined, + [4294966996, 4294967496], undefined, [168, 100], [64, 96], [0, 32], 4, + classificationNodesMatcher('Native heap', [true, true])); + + const discountedTracingOverheadRow = nativeHeapRow.subRows[3]; + checkRow(columns, discountedTracingOverheadRow, + '[discounted tracing overhead]', undefined, [-500, undefined], + undefined, [-32, undefined], [-32, undefined], undefined, undefined, + vmRegionsMatcher('[discounted tracing overhead]', undefined, + [true, false])); + + const filesRow = totalRow.subRows[2]; + checkRow(columns, filesRow, 'Files', undefined, [538968064, 1074077696], + undefined, [8192, 41984], undefined, undefined, 2, + classificationNodesMatcher('Files', [true, true])); + + const soRow = filesRow.subRows[0]; + checkRow(columns, soRow, 'so', undefined, [538968064, 1073741824], + undefined, [8192, 19456], undefined, undefined, 3, + classificationNodesMatcher('so', [true, true])); + + const mmapChromeRow = soRow.subRows[0]; + checkRow(columns, mmapChromeRow, '/lib/chrome.so', ['0000000000010000'], + [536870912, 536870912], ['r-xp', 'r-xp'], [8192, 9216], undefined, + undefined, undefined, + vmRegionsMatcher('/lib/chrome.so', 65536, [true, true])); + + const mmapLibX11Row = soRow.subRows[1]; + checkRow(columns, mmapLibX11Row, + '/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0', ['00007f996fd80000'], + [2097152, undefined], ['---p', undefined], [0, undefined], undefined, + undefined, undefined, + vmRegionsMatcher('/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0', + 140296983150592, [true, false])); + + const otherRow = totalRow.subRows[3]; + checkRow(columns, otherRow, 'Other', undefined, [undefined, 1], undefined, + [undefined, 1], [undefined, 1], [undefined, 1], 1, + classificationNodesMatcher('Other', [false, true])); + + const anotherMapRow = otherRow.subRows[0]; + checkRow(columns, anotherMapRow, 'another-map', ['00bad00bad00bad0'], + [undefined, 1], undefined, [undefined, 1], [undefined, 1], + [undefined, 1], undefined, + vmRegionsMatcher('another-map', 52583094233905872, [false, true])); + }); + + test('instantiate_multipleMax', function() { + const vmRegions = createVMRegions(); + + const viewEl = document.createElement( + 'tr-ui-a-memory-dump-vm-regions-details-pane'); + viewEl.vmRegions = vmRegions; + viewEl.aggregationMode = AggregationMode.MAX; + viewEl.rebuild(); + this.addHTMLOutput(viewEl); + + // Check that the table is shown. + assert.isTrue(isElementDisplayed(viewEl.$.table)); + assert.isFalse(isElementDisplayed(viewEl.$.info_text)); + + // Just check that the aggregation mode was propagated to the columns. + const table = viewEl.$.table; + const columns = table.tableColumns; + checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.MAX); + const rows = table.tableRows; + assert.lengthOf(rows, 1); + }); + + test('instantiate_multipleWithUndefined', function() { + const vmRegions = createVMRegions(); + vmRegions.splice(1, 0, undefined); + + const viewEl = document.createElement( + 'tr-ui-a-memory-dump-vm-regions-details-pane'); + viewEl.vmRegions = vmRegions; + viewEl.aggregationMode = AggregationMode.DIFF; + viewEl.rebuild(); + this.addHTMLOutput(viewEl); + + // Check that the table is shown. + assert.isTrue(isElementDisplayed(viewEl.$.table)); + assert.isFalse(isElementDisplayed(viewEl.$.info_text)); + + // Just check that the table has the right shape. + const table = viewEl.$.table; + const columns = table.tableColumns; + checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.DIFF); + const rows = table.tableRows; + assert.lengthOf(rows, 1); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view.html new file mode 100644 index 00000000000..0bb39b7e0f2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/related_events.html"> + +<dom-module id='tr-ui-a-multi-async-slice-sub-view'> + <template> + <style> + :host { + display: flex; + } + #container { + display: flex; + flex: 1 1 auto; + } + #events { + margin-left: 8px; + flex: 0 1 200px; + } + </style> + <div id="container"> + <tr-ui-a-multi-event-sub-view id="content"></tr-ui-a-multi-event-sub-view> + <div id="events"> + <tr-ui-a-related-events id="relatedEvents"></tr-ui-a-related-events> + </div> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-multi-async-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.content.selection = selection; + this.$.relatedEvents.setRelatedEvents(selection); + if (this.$.relatedEvents.hasRelatedEvents()) { + this.$.relatedEvents.style.display = ''; + } else { + this.$.relatedEvents.style.display = 'none'; + } + }, + + get relatedEventsToHighlight() { + if (!this.$.content.selection) return undefined; + + const selection = new tr.model.EventSet(); + this.$.content.selection.forEach(function(asyncEvent) { + if (!asyncEvent.associatedEvents) return; + + asyncEvent.associatedEvents.forEach(function(event) { + selection.push(event); + }); + }); + if (selection.length) return selection; + return undefined; + } +}); +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-multi-async-slice-sub-view', + tr.model.AsyncSlice, + { + multi: true, + title: 'Async Slices', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view_test.html new file mode 100644 index 00000000000..20fb52c058f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view_test.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/multi_async_slice_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx; + + test('instantiate', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.asyncSliceGroup.push(newAsyncSliceEx({ + title: 'a', + start: 10, + end: 20, + startThread: t1, + endThread: t1 + })); + t1.asyncSliceGroup.push(newAsyncSliceEx({ + title: 'b', + start: 25, + end: 40, + startThread: t1, + endThread: t1 + })); + + const selection = new tr.model.EventSet(); + selection.push(t1.asyncSliceGroup.slices[0]); + selection.push(t1.asyncSliceGroup.slices[1]); + + const viewEl = document.createElement('tr-ui-a-multi-async-slice-sub-view'); + viewEl.selection = selection; + this.addHTMLOutput(viewEl); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view.html new file mode 100644 index 00000000000..4525df0e8c2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html"> + +<dom-module id='tr-ui-a-multi-cpu-slice-sub-view'> + <template> + <style> + :host { + display: flex; + } + #content { + flex: 1 1 auto; + } + </style> + <tr-ui-a-multi-event-sub-view id="content"></tr-ui-a-multi-event-sub-view> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-multi-cpu-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + ready() { + this.$.content.eventsHaveSubRows = false; + }, + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.content.setSelectionWithoutErrorChecks(selection); + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-multi-cpu-slice-sub-view', + tr.model.CpuSlice, + { + multi: true, + title: 'CPU Slices', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view_test.html new file mode 100644 index 00000000000..36dc99bd338 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view_test.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/multi_cpu_slice_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createBasicModel() { + const lines = [ + 'Android.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck + ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck + 'Android.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck + 'Android.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck + ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck + ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E', + ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck + ]; + + return tr.c.TestUtils.newModelWithEvents([lines.join('\n')], { + shiftWorldToZero: false + }); + } + + test('instantiate', function() { + const m = createBasicModel(); + const cpu = m.kernel.cpus[1]; + assert.isDefined(cpu); + + const selection = new tr.model.EventSet(); + selection.push(cpu.slices[0]); + selection.push(cpu.slices[1]); + + const viewEl = document.createElement('tr-ui-a-multi-cpu-slice-sub-view'); + viewEl.selection = selection; + this.addHTMLOutput(viewEl); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view.html new file mode 100644 index 00000000000..52908c620bc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view.html @@ -0,0 +1,211 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_summary_table.html"> +<link rel="import" href="/tracing/ui/analysis/selection_summary_table.html"> +<link rel="import" href="/tracing/ui/base/radio_picker.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/value/diagnostics/scalar.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/ui/histogram_span.html"> + +<dom-module id='tr-ui-a-multi-event-sub-view'> + <template> + <style> + :host { + display: flex; + overflow: auto; + } + #content { + display: flex; + flex-direction: column; + flex: 0 1 auto; + align-self: stretch; + } + #content > * { + flex: 0 0 auto; + align-self: stretch; + } + #histogramContainer { + display: flex; + } + + tr-ui-a-multi-event-summary-table { + border-bottom: 1px solid #aaa; + } + + tr-ui-a-selection-summary-table { + margin-top: 1.25em; + border-top: 1px solid #aaa; + background-color: #eee; + font-weight: bold; + margin-bottom: 1.25em; + border-bottom: 1px solid #aaa; + } + </style> + <div id="content"> + <tr-ui-a-multi-event-summary-table id="eventSummaryTable"> + </tr-ui-a-multi-event-summary-table> + <tr-ui-a-selection-summary-table id="selectionSummaryTable"> + </tr-ui-a-selection-summary-table> + <tr-ui-b-radio-picker id="radioPicker"> + </tr-ui-b-radio-picker> + <div id="histogramContainer"> + <tr-v-ui-histogram-span id="histogramSpan"> + </tr-v-ui-histogram-span> + </div> + </div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const EVENT_FIELD = [ + {key: 'start', label: 'Start'}, + {key: 'cpuDuration', label: 'CPU Duration'}, + {key: 'duration', label: 'Duration'}, + {key: 'cpuSelfTime', label: 'CPU Self Time'}, + {key: 'selfTime', label: 'Self Time'} + ]; + + function buildDiagnostics_(slice) { + const diagnostics = {}; + for (const item of EVENT_FIELD) { + const fieldName = item.key; + if (slice[fieldName] === undefined) continue; + diagnostics[fieldName] = new tr.v.d.Scalar(new tr.b.Scalar( + tr.b.Unit.byName.timeDurationInMs, slice[fieldName])); + } + diagnostics.args = new tr.v.d.GenericSet([slice.args]); + diagnostics.event = new tr.v.d.RelatedEventSet(slice); + return diagnostics; + } + + Polymer({ + is: 'tr-ui-a-multi-event-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.currentSelection_ = undefined; + this.eventsHaveDuration_ = true; + this.eventsHaveSubRows_ = true; + }, + + ready() { + this.$.radioPicker.style.display = 'none'; + this.$.radioPicker.items = EVENT_FIELD; + this.$.radioPicker.select('cpuSelfTime'); + this.$.radioPicker.addEventListener('change', () => { + if (this.isAttached) this.updateContents_(); + }); + + this.$.histogramSpan.graphWidth = 400; + this.$.histogramSpan.canMergeSampleDiagnostics = false; + this.$.histogramContainer.style.display = 'none'; + }, + + attached() { + if (this.currentSelection_ !== undefined) this.updateContents_(); + }, + + set selection(selection) { + if (selection.length <= 1) { + throw new Error('Only supports multiple items'); + } + this.setSelectionWithoutErrorChecks(selection); + }, + + get selection() { + return this.currentSelection_; + }, + + setSelectionWithoutErrorChecks(selection) { + this.currentSelection_ = selection; + if (this.isAttached) this.updateContents_(); + }, + + get eventsHaveDuration() { + return this.eventsHaveDuration_; + }, + + set eventsHaveDuration(eventsHaveDuration) { + this.eventsHaveDuration_ = eventsHaveDuration; + if (this.isAttached) this.updateContents_(); + }, + + get eventsHaveSubRows() { + return this.eventsHaveSubRows_; + }, + + set eventsHaveSubRows(eventsHaveSubRows) { + this.eventsHaveSubRows_ = eventsHaveSubRows; + if (this.isAttached) this.updateContents_(); + }, + + buildHistogram_(selectedKey) { + let leftBoundary = Number.MAX_VALUE; + let rightBoundary = tr.b.math.Statistics.percentile( + this.currentSelection_, 0.95, + function(value) { + leftBoundary = Math.min(leftBoundary, value[selectedKey]); + return value[selectedKey]; + }); + + if (leftBoundary === rightBoundary) rightBoundary += 1; + const histogram = new tr.v.Histogram( + '', + tr.b.Unit.byName.timeDurationInMs, + tr.v.HistogramBinBoundaries.createLinear( + leftBoundary, rightBoundary, + Math.ceil(Math.sqrt(this.currentSelection_.length)))); + histogram.customizeSummaryOptions({sum: false}); + for (const slice of this.currentSelection_) { + histogram.addSample(slice[selectedKey], + buildDiagnostics_(slice)); + } + + return histogram; + }, + + updateContents_() { + const selection = this.currentSelection_; + if (!selection) return; + + const eventsByTitle = selection.getEventsOrganizedByTitle(); + const numTitles = Object.keys(eventsByTitle).length; + + this.$.eventSummaryTable.configure({ + showTotals: numTitles > 1, + eventsByTitle, + eventsHaveDuration: this.eventsHaveDuration_, + eventsHaveSubRows: this.eventsHaveSubRows_ + }); + + this.$.selectionSummaryTable.selection = this.currentSelection_; + + if (numTitles === 1) { + this.$.radioPicker.style.display = 'block'; + this.$.histogramContainer.style.display = 'flex'; + this.$.histogramSpan.build( + this.buildHistogram_(this.$.radioPicker.selectedKey)); + if (this.$.histogramSpan.histogram.numValues === 0) { + this.$.histogramContainer.style.display = 'none'; + } + } else { + this.$.radioPicker.style.display = 'none'; + this.$.histogramContainer.style.display = 'none'; + } + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view_test.html new file mode 100644 index 00000000000..9958b7db81c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view_test.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const Thread = tr.model.Thread; + const EventSet = tr.model.EventSet; + const newSliceEx = tr.c.TestUtils.newSliceEx; + + test('differentTitles', function() { + const model = new Model(); + const t53 = model.getOrCreateProcess(52).getOrCreateThread(53); + t53.sliceGroup.pushSlice(newSliceEx( + {title: 'a', start: 0.0, duration: 0.04})); + t53.sliceGroup.pushSlice(newSliceEx( + {title: 'a', start: 0.12, duration: 0.06})); + t53.sliceGroup.pushSlice(newSliceEx( + {title: 'aa', start: 0.5, duration: 0.5})); + t53.sliceGroup.createSubSlices(); + + const t53track = {}; + t53track.thread = t53; + + const selection = new EventSet(); + selection.push(t53.sliceGroup.slices[0]); + selection.push(t53.sliceGroup.slices[1]); + selection.push(t53.sliceGroup.slices[2]); + + const viewEl = document.createElement('tr-ui-a-multi-event-sub-view'); + viewEl.selection = selection; + this.addHTMLOutput(viewEl); + + const summaryTableEl = tr.ui.b.findDeepElementMatching( + viewEl, 'tr-ui-a-multi-event-summary-table'); + assert.isTrue(summaryTableEl.showTotals); + assert.lengthOf(Object.keys(summaryTableEl.eventsByTitle), 2); + + const selectionSummaryTableEl = tr.ui.b.findDeepElementMatching( + viewEl, 'tr-ui-a-selection-summary-table'); + assert.strictEqual(selectionSummaryTableEl.selection, selection); + + const radioPickerEl = + tr.ui.b.findDeepElementMatching(viewEl, 'tr-ui-b-radio-picker'); + assert.strictEqual(radioPickerEl.style.display, 'none'); + }); + + test('sameTitles', function() { + const model = new Model(); + const t53 = model.getOrCreateProcess(52).getOrCreateThread(53); + t53.sliceGroup.pushSlice(newSliceEx( + {title: 'c', start: 0.0, duration: 0.04})); + t53.sliceGroup.pushSlice(newSliceEx( + {title: 'c', start: 0.12, duration: 0.06})); + t53.sliceGroup.createSubSlices(); + + const t53track = {}; + t53track.thread = t53; + + const selection = new EventSet(); + selection.push(t53.sliceGroup.slices[0]); + selection.push(t53.sliceGroup.slices[1]); + + const viewEl = document.createElement('tr-ui-a-multi-event-sub-view'); + viewEl.selection = selection; + this.addHTMLOutput(viewEl); + + const summaryTableEl = tr.ui.b.findDeepElementMatching( + viewEl, 'tr-ui-a-multi-event-summary-table'); + assert.isFalse(summaryTableEl.showTotals); + assert.lengthOf(Object.keys(summaryTableEl.eventsByTitle), 1); + + const selectionSummaryTableEl = tr.ui.b.findDeepElementMatching( + viewEl, 'tr-ui-a-selection-summary-table'); + assert.strictEqual(selectionSummaryTableEl.selection, selection); + + const radioPickerEl = + tr.ui.b.findDeepElementMatching(viewEl, 'tr-ui-b-radio-picker'); + assert.strictEqual(radioPickerEl.style.display, 'block'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary.html new file mode 100644 index 00000000000..886e315863e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary.html @@ -0,0 +1,207 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/base/utils.html"> + +<script> +'use strict'; +tr.exportTo('tr.ui.analysis', function() { + function MultiEventSummary(title, events) { + this.title = title; + this.duration_ = undefined; + this.selfTime_ = undefined; + this.events_ = events; + + this.cpuTimesComputed_ = false; + this.cpuSelfTime_ = undefined; + this.cpuDuration_ = undefined; + + this.maxDuration_ = undefined; + this.maxCpuDuration_ = undefined; + this.maxSelfTime_ = undefined; + this.maxCpuSelfTime_ = undefined; + + this.untotallableArgs_ = []; + this.totalledArgs_ = undefined; + } + MultiEventSummary.prototype = { + + set title(title) { + if (title === 'Totals') { + this.totalsRow = true; + } + this.title_ = title; + }, + + get title() { + return this.title_; + }, + + get duration() { + if (this.duration_ === undefined) { + this.duration_ = tr.b.math.Statistics.sum( + this.events_, function(event) { + return event.duration; + }); + } + return this.duration_; + }, + + get cpuSelfTime() { + this.computeCpuTimesIfNeeded_(); + return this.cpuSelfTime_; + }, + + get cpuDuration() { + this.computeCpuTimesIfNeeded_(); + return this.cpuDuration_; + }, + + computeCpuTimesIfNeeded_() { + if (this.cpuTimesComputed_) return; + this.cpuTimesComputed_ = true; + + let cpuSelfTime = 0; + let cpuDuration = 0; + let hasCpuData = false; + for (const event of this.events_) { + if (event.cpuDuration !== undefined) { + cpuDuration += event.cpuDuration; + hasCpuData = true; + } + + if (event.cpuSelfTime !== undefined) { + cpuSelfTime += event.cpuSelfTime; + hasCpuData = true; + } + } + if (hasCpuData) { + this.cpuDuration_ = cpuDuration; + this.cpuSelfTime_ = cpuSelfTime; + } + }, + + get selfTime() { + if (this.selfTime_ === undefined) { + this.selfTime_ = 0; + for (const event of this.events_) { + if (event.selfTime !== undefined) { + this.selfTime_ += event.selfTime; + } + } + } + return this.selfTime_; + }, + + get events() { + return this.events_; + }, + + get numEvents() { + return this.events_.length; + }, + + get numAlerts() { + if (this.numAlerts_ === undefined) { + this.numAlerts_ = tr.b.math.Statistics.sum(this.events_, event => + event.associatedAlerts.length + ); + } + return this.numAlerts_; + }, + + get untotallableArgs() { + this.updateArgsIfNeeded_(); + return this.untotallableArgs_; + }, + + get totalledArgs() { + this.updateArgsIfNeeded_(); + return this.totalledArgs_; + }, + + + get maxDuration() { + if (this.maxDuration_ === undefined) { + this.maxDuration_ = tr.b.math.Statistics.max( + this.events_, function(event) { + return event.duration; + }); + } + return this.maxDuration_; + }, + + + get maxCpuDuration() { + if (this.maxCpuDuration_ === undefined) { + this.maxCpuDuration_ = tr.b.math.Statistics.max( + this.events_, function(event) { + return event.cpuDuration; + }); + } + return this.maxCpuDuration_; + }, + + + get maxSelfTime() { + if (this.maxSelfTime_ === undefined) { + this.maxSelfTime_ = tr.b.math.Statistics.max( + this.events_, function(event) { + return event.selfTime; + }); + } + return this.maxSelfTime_; + }, + + + get maxCpuSelfTime() { + if (this.maxCpuSelfTime_ === undefined) { + this.maxCpuSelfTime_ = tr.b.math.Statistics.max( + this.events_, function(event) { + return event.cpuSelfTime; + }); + } + return this.maxCpuSelfTime_; + }, + + + updateArgsIfNeeded_() { + if (this.totalledArgs_ !== undefined) return; + + const untotallableArgs = {}; + const totalledArgs = {}; + for (const event of this.events_) { + for (const argName in event.args) { + const argVal = event.args[argName]; + const type = typeof argVal; + if (type !== 'number') { + untotallableArgs[argName] = true; + delete totalledArgs[argName]; + continue; + } + if (untotallableArgs[argName]) { + continue; + } + + if (totalledArgs[argName] === undefined) { + totalledArgs[argName] = 0; + } + totalledArgs[argName] += argVal; + } + } + this.untotallableArgs_ = Object.keys(untotallableArgs); + this.totalledArgs_ = totalledArgs; + } + }; + + return { + MultiEventSummary, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table.html new file mode 100644 index 00000000000..1b32d606f61 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table.html @@ -0,0 +1,358 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_summary.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-multi-event-summary-table'> + <template> + <style> + :host { + display: flex; + } + #table { + flex: 1 1 auto; + align-self: stretch; + font-size: 12px; + } + </style> + <tr-ui-b-table id="table"> + </tr-ui-b-table> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-multi-event-summary-table', + + ready() { + this.showTotals_ = false; + this.eventsHaveDuration_ = true; + this.eventsHaveSubRows_ = true; + this.eventsByTitle_ = undefined; + }, + + updateTableColumns_(rows, maxValues) { + let hasCpuData = false; + let hasAlerts = false; + rows.forEach(function(row) { + if (row.cpuDuration !== undefined) { + hasCpuData = true; + } + if (row.cpuSelfTime !== undefined) { + hasCpuData = true; + } + if (row.numAlerts) { + hasAlerts = true; + } + }); + + const ownerDocument = this.ownerDocument; + + const columns = []; + + columns.push({ + title: 'Name', + value(row) { + if (row.title === 'Totals') return 'Totals'; + const container = document.createElement('div'); + const linkEl = document.createElement('tr-ui-a-analysis-link'); + linkEl.setSelectionAndContent(function() { + return new tr.model.EventSet(row.events); + }, row.title); + container.appendChild(linkEl); + + if (tr.isExported('tr-ui-e-chrome-codesearch')) { + const link = document.createElement('tr-ui-e-chrome-codesearch'); + link.searchPhrase = row.title; + container.appendChild(link); + } + return container; + }, + width: '350px', + cmp(rowA, rowB) { + return rowA.title.localeCompare(rowB.title); + } + }); + if (this.eventsHaveDuration_) { + columns.push({ + title: 'Wall Duration', + value(row) { + return tr.v.ui.createScalarSpan(row.duration, { + unit: tr.b.Unit.byName.timeDurationInMs, + customContextRange: row.totalsRow ? undefined : + tr.b.math.Range.fromExplicitRange(0, maxValues.duration), + ownerDocument, + }); + }, + width: '<upated further down>', + cmp(rowA, rowB) { + return rowA.duration - rowB.duration; + } + }); + } + + if (this.eventsHaveDuration_ && hasCpuData) { + columns.push({ + title: 'CPU Duration', + value(row) { + return tr.v.ui.createScalarSpan(row.cpuDuration, { + unit: tr.b.Unit.byName.timeDurationInMs, + customContextRange: row.totalsRow ? undefined : + tr.b.math.Range.fromExplicitRange(0, maxValues.cpuDuration), + ownerDocument, + }); + }, + width: '<upated further down>', + cmp(rowA, rowB) { + return rowA.cpuDuration - rowB.cpuDuration; + } + }); + } + + if (this.eventsHaveSubRows_ && this.eventsHaveDuration_) { + columns.push({ + title: 'Self time', + value(row) { + return tr.v.ui.createScalarSpan(row.selfTime, { + unit: tr.b.Unit.byName.timeDurationInMs, + customContextRange: row.totalsRow ? undefined : + tr.b.math.Range.fromExplicitRange(0, maxValues.selfTime), + ownerDocument, + }); + }, + width: '<upated further down>', + cmp(rowA, rowB) { + return rowA.selfTime - rowB.selfTime; + } + }); + } + + if (this.eventsHaveSubRows_ && this.eventsHaveDuration_ && hasCpuData) { + columns.push({ + title: 'CPU Self Time', + value(row) { + return tr.v.ui.createScalarSpan(row.cpuSelfTime, { + unit: tr.b.Unit.byName.timeDurationInMs, + customContextRange: row.totalsRow ? undefined : + tr.b.math.Range.fromExplicitRange(0, maxValues.cpuSelfTime), + ownerDocument, + }); + }, + width: '<upated further down>', + cmp(rowA, rowB) { + return rowA.cpuSelfTime - rowB.cpuSelfTime; + } + }); + } + + if (this.eventsHaveDuration_) { + columns.push({ + title: 'Average ' + (hasCpuData ? 'CPU' : 'Wall') + ' Duration', + value(row) { + const totalDuration = hasCpuData ? row.cpuDuration : row.duration; + return tr.v.ui.createScalarSpan(totalDuration / row.numEvents, { + unit: tr.b.Unit.byName.timeDurationInMs, + customContextRange: row.totalsRow ? undefined : + tr.b.math.Range.fromExplicitRange(0, maxValues.duration), + ownerDocument, + }); + }, + width: '<upated further down>', + cmp(rowA, rowB) { + if (hasCpuData) { + return rowA.cpuDuration / rowA.numEvents - + rowB.cpuDuration / rowB.numEvents; + } + return rowA.duration / rowA.numEvents - + rowB.duration / rowB.numEvents; + } + }); + } + + columns.push({ + title: 'Occurrences', + value(row) { + return row.numEvents; + }, + width: '<upated further down>', + cmp(rowA, rowB) { + return rowA.numEvents - rowB.numEvents; + } + }); + + let alertsColumnIndex; + if (hasAlerts) { + columns.push({ + title: 'Num Alerts', + value(row) { + return row.numAlerts; + }, + width: '<upated further down>', + cmp(rowA, rowB) { + return rowA.numAlerts - rowB.numAlerts; + } + }); + alertsColumnIndex = columns.length - 1; + } + let colWidthPercentage; + if (columns.length === 1) { + colWidthPercentage = '100%'; + } else { + colWidthPercentage = (100 / (columns.length - 1)).toFixed(3) + '%'; + } + + for (let i = 1; i < columns.length; i++) { + columns[i].width = colWidthPercentage; + } + + this.$.table.tableColumns = columns; + + if (hasAlerts) { + this.$.table.sortColumnIndex = alertsColumnIndex; + this.$.table.sortDescending = true; + } + }, + + configure(config) { + if (config.eventsByTitle === undefined) { + throw new Error('Required: eventsByTitle'); + } + + if (config.showTotals !== undefined) { + this.showTotals_ = config.showTotals; + } else { + this.showTotals_ = true; + } + + if (config.eventsHaveDuration !== undefined) { + this.eventsHaveDuration_ = config.eventsHaveDuration; + } else { + this.eventsHaveDuration_ = true; + } + + if (config.eventsHaveSubRows !== undefined) { + this.eventsHaveSubRows_ = config.eventsHaveSubRows; + } else { + this.eventsHaveSubRows_ = true; + } + + this.eventsByTitle_ = config.eventsByTitle; + this.updateContents_(); + }, + + get showTotals() { + return this.showTotals_; + }, + + set showTotals(showTotals) { + this.showTotals_ = showTotals; + this.updateContents_(); + }, + + get eventsHaveDuration() { + return this.eventsHaveDuration_; + }, + + set eventsHaveDuration(eventsHaveDuration) { + this.eventsHaveDuration_ = eventsHaveDuration; + this.updateContents_(); + }, + + get eventsHaveSubRows() { + return this.eventsHaveSubRows_; + }, + + set eventsHaveSubRows(eventsHaveSubRows) { + this.eventsHaveSubRows_ = eventsHaveSubRows; + this.updateContents_(); + }, + + get eventsByTitle() { + return this.eventsByTitle_; + }, + + set eventsByTitle(eventsByTitle) { + this.eventsByTitle_ = eventsByTitle; + this.updateContents_(); + }, + + get selectionBounds() { + return this.selectionBounds_; + }, + + set selectionBounds(selectionBounds) { + this.selectionBounds_ = selectionBounds; + this.updateContents_(); + }, + + updateContents_() { + let eventsByTitle; + if (this.eventsByTitle_ !== undefined) { + eventsByTitle = this.eventsByTitle_; + } else { + eventsByTitle = []; + } + + const allEvents = new tr.model.EventSet(); + const rows = []; + for (const [title, eventsOfSingleTitle] of Object.entries(eventsByTitle)) { + for (const event of eventsOfSingleTitle) allEvents.push(event); + const row = new tr.ui.analysis.MultiEventSummary( + title, eventsOfSingleTitle); + rows.push(row); + } + + this.updateTableColumns_(rows); + this.$.table.tableRows = rows; + + const maxValues = { + duration: undefined, + selfTime: undefined, + cpuSelfTime: undefined, + cpuDuration: undefined + }; + + if (this.eventsHaveDuration) { + for (const column in maxValues) { + maxValues[column] = tr.b.math.Statistics.max(rows, function(event) { + return event[column]; + }); + } + } + + const footerRows = []; + + if (this.showTotals_) { + const multiEventSummary = new tr.ui.analysis.MultiEventSummary( + 'Totals', allEvents); + footerRows.push(multiEventSummary); + } + + + this.updateTableColumns_(rows, maxValues); + this.$.table.tableRows = rows; + + // TODO(selection bounds). + + // TODO(sorting) + + this.$.table.footerRows = footerRows; + this.$.table.rebuild(); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table_test.html new file mode 100644 index 00000000000..32efc0de1ff --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table_test.html @@ -0,0 +1,119 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_summary_table.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const EventSet = tr.model.EventSet; + const newSliceEx = tr.c.TestUtils.newSliceEx; + + test('basicNoCpu', function() { + const model = new Model(); + const thread = model.getOrCreateProcess(1).getOrCreateThread(2); + const tsg = thread.sliceGroup; + tsg.pushSlice(newSliceEx({title: 'a', start: 0, duration: 0.5})); + tsg.pushSlice(newSliceEx({title: 'b', start: 1, duration: 0.5})); + tsg.pushSlice(newSliceEx({title: 'b', start: 2, duration: 0.5})); + tsg.createSubSlices(); + + const threadTrack = {}; + threadTrack.thread = thread; + + const selection = new EventSet(tsg.slices); + + const viewEl = document.createElement('tr-ui-a-multi-event-summary-table'); + viewEl.configure({ + showTotals: true, + eventsHaveDuration: true, + eventsByTitle: selection.getEventsOrganizedByTitle() + }); + this.addHTMLOutput(viewEl); + }); + + test('basicWithCpu', function() { + const model = new Model(); + const thread = model.getOrCreateProcess(1).getOrCreateThread(2); + const tsg = thread.sliceGroup; + tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3, + cpuStart: 0, cpuEnd: 3})); + tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2, + cpuStart: 1, cpuEnd: 1.75})); + tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5, + cpuStart: 3, cpuEnd: 3.75})); + tsg.createSubSlices(); + + const threadTrack = {}; + threadTrack.thread = thread; + + const selection = new EventSet(tsg.slices); + + const viewEl = document.createElement('tr-ui-a-multi-event-summary-table'); + viewEl.configure({ + showTotals: true, + eventsHaveDuration: true, + eventsByTitle: selection.getEventsOrganizedByTitle() + }); + this.addHTMLOutput(viewEl); + + const totals = tr.ui.b.findDeepElementMatchingPredicate( + viewEl, e => e.tagName === 'TFOOT'); + const scalars = tr.ui.b.findDeepElementsMatchingPredicate( + totals, e => e.tagName === 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(scalars[0].value, 5); + assert.closeTo(scalars[1].value, 4.5, 1e-6); + assert.strictEqual(scalars[2].value, 4); + assert.closeTo(scalars[3].value, 3.75, 1e-6); + assert.closeTo(scalars[4].value, 1.5, 1e-6); + assert.strictEqual('3', totals.children[0].children[6].textContent); + }); + + test('noSelfTimeNoSubRows', function() { + const model = new Model(); + + const fe1 = new tr.model.FlowEvent('cat', 1234, 'title', 7, 10, {}); + const fe2 = new tr.model.FlowEvent('cat', 1234, 'title', 8, 20, {}); + + // Make reading some properties an explosion, as a way to ensure that they + // aren't read. Note that 'duration' is read since it is used by the + // EventSet to get the range. + const failProp = { + get() { + throw new Error('Should not be called'); + } + }; + Object.defineProperty(fe1, 'subRows', failProp); + Object.defineProperty(fe2, 'subRows', failProp); + + Object.defineProperty(fe1, 'selfTime', failProp); + Object.defineProperty(fe2, 'selfTime', failProp); + + model.flowEvents.push(fe1); + model.flowEvents.push(fe2); + + const selection = new EventSet([fe1, fe2]); + + const viewEl = document.createElement('tr-ui-a-multi-event-summary-table'); + viewEl.configure({ + showTotals: true, + eventsHaveDuration: false, + eventsHaveSubRows: false, + eventsByTitle: selection.getEventsOrganizedByTitle() + }); + this.addHTMLOutput(viewEl); + }); + + // TODO(nduca): Tooltippish stuff. +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_test.html new file mode 100644 index 00000000000..fcc73e1d608 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_test.html @@ -0,0 +1,111 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_summary.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const newSliceEx = tr.c.TestUtils.newSliceEx; + + test('summaryRowNoCpu', function() { + const model = new Model(); + const thread = model.getOrCreateProcess(1).getOrCreateThread(2); + const tsg = thread.sliceGroup; + + tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3})); + tsg.pushSlice(newSliceEx({title: 'bb', start: 1, end: 2})); + tsg.pushSlice(newSliceEx({title: 'bb', start: 4, end: 5})); + tsg.createSubSlices(); + + const row = new tr.ui.analysis.MultiEventSummary('x', tsg.slices.slice(0)); + assert.strictEqual(row.duration, 5); + assert.strictEqual(row.selfTime, 4); + assert.isUndefined(row.cpuDuration); + assert.isUndefined(row.cpuSelfTime); + }); + + test('summaryRowWithCpu', function() { + const model = new Model(); + const thread = model.getOrCreateProcess(1).getOrCreateThread(2); + const tsg = thread.sliceGroup; + + tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3, + cpuStart: 0, cpuEnd: 3})); + tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2, + cpuStart: 1, cpuEnd: 1.75})); + tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5, + cpuStart: 3, cpuEnd: 3.75})); + tsg.createSubSlices(); + + const row = new tr.ui.analysis.MultiEventSummary('x', tsg.slices.slice(0)); + assert.strictEqual(row.duration, 5); + assert.strictEqual(row.selfTime, 4); + assert.strictEqual(row.cpuDuration, 4.5); + assert.strictEqual(row.cpuSelfTime, 3.75); + assert.strictEqual(row.maxDuration, 3); + assert.strictEqual(row.maxSelfTime, 2); + assert.strictEqual(row.maxCpuDuration, 3); + assert.strictEqual(row.maxCpuSelfTime, 2.25); + }); + + test('summaryRowNonSlice', function() { + const model = new Model(); + const thread = model.getOrCreateProcess(1).getOrCreateThread(2); + const tsg = thread.sliceGroup; + + const fe1 = new tr.model.FlowEvent('cat', 1234, 'title', 7, 10, {}); + const fe2 = new tr.model.FlowEvent('cat', 1234, 'title', 8, 20, {}); + model.flowEvents.push(fe1); + model.flowEvents.push(fe2); + + const row = new tr.ui.analysis.MultiEventSummary('a', [fe1, fe2]); + assert.strictEqual(row.duration, 0); + assert.strictEqual(row.selfTime, 0); + assert.isUndefined(row.cpuDuration); + assert.isUndefined(row.cpuSelfTime); + assert.strictEqual(row.maxDuration, 0); + }); + + test('summaryNumAlerts', function() { + const slice = newSliceEx({title: 'b', start: 0, duration: 0.002}); + + const ALERT_INFO_1 = new tr.model.EventInfo( + 'Alert 1', 'Critical alert'); + + const alert = new tr.model.Alert(ALERT_INFO_1, 5, [slice]); + + const row = new tr.ui.analysis.MultiEventSummary('a', [slice]); + assert.strictEqual(row.numAlerts, 1); + }); + + test('argSummary', function() { + const model = new Model(); + const thread = model.getOrCreateProcess(1).getOrCreateThread(2); + const tsg = thread.sliceGroup; + + tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3, + args: {value1: 3, value2: 'x', value3: 1}})); + tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2, + args: {value1: 3, value2: 'y', value3: 2}})); + tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5, + args: {value1: 3, value2: 'z', value3: 'x'}})); + tsg.createSubSlices(); + + const row = new tr.ui.analysis.MultiEventSummary('x', tsg.slices.slice(0)); + assert.deepEqual(row.totalledArgs, {value1: 9}); + assert.deepEqual(row.untotallableArgs, ['value2', 'value3']); + assert.strictEqual(row.maxDuration, 3); + assert.strictEqual(row.maxSelfTime, 2); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view.html new file mode 100644 index 00000000000..3e509508732 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html"> + +<dom-module id='tr-ui-a-multi-flow-event-sub-view'> + <template> + <style> + :host { + display: flex; + } + </style> + <tr-ui-a-multi-event-sub-view id="content"></tr-ui-a-multi-event-sub-view> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-multi-flow-event-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + ready() { + this.$.content.eventsHaveDuration = false; + this.$.content.eventsHaveSubRows = false; + }, + + set selection(selection) { + this.$.content.selection = selection; + }, + + get selection() { + return this.$.content.selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-multi-flow-event-sub-view', + tr.model.FlowEvent, + { + multi: true, + title: 'Flow Events', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view_test.html new file mode 100644 index 00000000000..8b1d98d7bd1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view_test.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const EventSet = tr.model.EventSet; + + test('analyzeSelectionWithSingleEvent', function() { + const model = new Model(); + + const fe1 = new tr.model.FlowEvent('cat', 1234, 'title', 7, 10, {}); + const fe2 = new tr.model.FlowEvent('cat', 1234, 'title', 8, 20, {}); + model.flowEvents.push(fe1); + model.flowEvents.push(fe2); + + const selection = new EventSet(); + selection.push(fe1); + selection.push(fe2); + assert.strictEqual(selection.length, 2); + + const subView = document.createElement('tr-ui-a-multi-flow-event-sub-view'); + subView.selection = selection; + + this.addHTMLOutput(subView); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_frame_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_frame_sub_view.html new file mode 100644 index 00000000000..cdf71245df3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_frame_sub_view.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html"> + +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-multi-frame-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.currentSelection_ = undefined; + }, + + set selection(selection) { + Polymer.dom(this).textContent = ''; + const realView = document.createElement('tr-ui-a-multi-event-sub-view'); + realView.eventsHaveDuration = false; + realView.eventsHaveSubRows = false; + + Polymer.dom(this).appendChild(realView); + realView.setSelectionWithoutErrorChecks(selection); + + this.currentSelection_ = selection; + }, + + get selection() { + return this.currentSelection_; + }, + + get relatedEventsToHighlight() { + if (!this.currentSelection_) return undefined; + + const selection = new tr.model.EventSet(); + this.currentSelection_.forEach(function(frameEvent) { + frameEvent.associatedEvents.forEach(function(event) { + selection.push(event); + }); + }); + return selection; + } +}); +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-multi-frame-sub-view', + tr.model.Frame, + { + multi: true, + title: 'Frames', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view.html new file mode 100644 index 00000000000..c2252f90e14 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html"> + +<dom-module id='tr-ui-a-multi-instant-event-sub-view'> + <template> + <style> + :host { + display: block; + } + </style> + <div id='content'></div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-multi-instant-event-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.currentSelection_ = undefined; + }, + + set selection(selection) { + Polymer.dom(this.$.content).textContent = ''; + const realView = document.createElement('tr-ui-a-multi-event-sub-view'); + realView.eventsHaveDuration = false; + realView.eventsHaveSubRows = false; + + Polymer.dom(this.$.content).appendChild(realView); + realView.setSelectionWithoutErrorChecks(selection); + + this.currentSelection_ = selection; + }, + + get selection() { + return this.currentSelection_; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view_test.html new file mode 100644 index 00000000000..ca228515de5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view_test.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const EventSet = tr.model.EventSet; + + test('analyzeSelectionWithSingleEvent', function() { + const model = new Model(); + const p52 = model.getOrCreateProcess(52); + const t53 = p52.getOrCreateThread(53); + + const ie1 = new tr.model.ProcessInstantEvent('cat', 'title', 7, 10, {}); + const ie2 = new tr.model.ProcessInstantEvent('cat', 'title', 7, 20, {}); + p52.instantEvents.push(ie1); + p52.instantEvents.push(ie2); + + + const selection = new EventSet(); + selection.push(ie1); + selection.push(ie2); + assert.strictEqual(selection.length, 2); + + const subView = + document.createElement('tr-ui-a-multi-instant-event-sub-view'); + subView.selection = selection; + + this.addHTMLOutput(subView); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view.html new file mode 100644 index 00000000000..089f4b011a2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view.html @@ -0,0 +1,112 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-multi-object-sub-view'> + <template> + <style> + :host { + display: flex; + font-size: 12px; + } + </style> + <tr-ui-b-table id="content"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-multi-object-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.currentSelection_ = undefined; + }, + + ready() { + this.$.content.showHeader = false; + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + this.currentSelection_ = selection; + + const objectEvents = Array.from(selection).sort( + tr.b.math.Range.compareByMinTimes); + + const timeSpanConfig = { + unit: tr.b.Unit.byName.timeStampInMs, + ownerDocument: this.ownerDocument + }; + const table = this.$.content; + table.tableColumns = [ + { + title: 'First', + value(event) { + if (event instanceof tr.model.ObjectSnapshot) { + return tr.v.ui.createScalarSpan(event.ts, timeSpanConfig); + } + + const spanEl = document.createElement('span'); + Polymer.dom(spanEl).appendChild(tr.v.ui.createScalarSpan( + event.creationTs, timeSpanConfig)); + Polymer.dom(spanEl).appendChild(tr.ui.b.createSpan({ + textContent: '-', + marginLeft: '4px', + marginRight: '4px' + })); + if (event.deletionTs !== Number.MAX_VALUE) { + Polymer.dom(spanEl).appendChild(tr.v.ui.createScalarSpan( + event.deletionTs, timeSpanConfig)); + } + return spanEl; + }, + width: '200px' + }, + { + title: 'Second', + value(event) { + const linkEl = document.createElement('tr-ui-a-analysis-link'); + linkEl.setSelectionAndContent(function() { + return new tr.model.EventSet(event); + }, event.userFriendlyName); + return linkEl; + }, + width: '100%' + } + ]; + table.tableRows = objectEvents; + table.rebuild(); + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-multi-object-sub-view', + tr.model.ObjectInstance, + { + multi: true, + title: 'Object Instances', + }); +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-multi-object-sub-view', + tr.model.ObjectSnapshot, + { + multi: true, + title: 'Object Snapshots', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view_test.html new file mode 100644 index 00000000000..18e62170e7d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view_test.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const EventSet = tr.model.EventSet; + const ObjectInstance = tr.model.ObjectInstance; + + test('instantiate_analysisWithObjects', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const objects = p1.objects; + const i10 = objects.idWasCreated( + '0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 10); + const s10 = objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', + 10, 'snapshot-1'); + const s25 = objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', + 25, 'snapshot-2'); + const s40 = objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', + 40, 'snapshot-3'); + objects.idWasDeleted('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 45); + + const track = {}; + const selection = new EventSet(); + selection.push(i10); + selection.push(s10); + selection.push(s25); + selection.push(s40); + + const analysisEl = document.createElement('tr-ui-a-multi-object-sub-view'); + analysisEl.selection = selection; + this.addHTMLOutput(analysisEl); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view.html new file mode 100644 index 00000000000..32315c220a1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view.html @@ -0,0 +1,77 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/frame_power_usage_chart.html"> +<link rel="import" href="/tracing/ui/analysis/power_sample_summary_table.html"> + +<dom-module id='tr-ui-a-multi-power-sample-sub-view'> + <template> + <style> + :host { + display: flex; + flex-direction: row; + } + #tables { + display: flex; + flex-direction: column; + width: 50%; + } + #chart { + width: 50%; + } + </style> + <div id="tables"> + <tr-ui-a-power-sample-summary-table id="summaryTable"> + </tr-ui-a-power-sample-summary-table> + </div> + <tr-ui-a-frame-power-usage-chart id="chart"> + </tr-ui-a-frame-power-usage-chart> + </template> +</dom-module> + +<script> +'use strict'; + +// TODO(charliea): Add a dropdown that allows the user to select which type of +// power sample analysis view they want (e.g. table of samples, graph). +Polymer({ + is: 'tr-ui-a-multi-power-sample-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + ready() { + this.currentSelection_ = undefined; + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + this.currentSelection_ = selection; + this.updateContents_(); + }, + + updateContents_() { + const samples = this.selection; + const vSyncTimestamps = (!samples ? [] : + tr.b.getFirstElement(samples).series.device.vSyncTimestamps); + + this.$.summaryTable.samples = samples; + this.$.chart.setData(this.selection, vSyncTimestamps); + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-multi-power-sample-sub-view', + tr.model.PowerSample, + { + multi: true, + title: 'Power Samples', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view_test.html new file mode 100644 index 00000000000..f7759572e28 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view_test.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/power_series.html"> +<link rel="import" href="/tracing/ui/analysis/multi_power_sample_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate_noSamplesOrVSyncs', function() { + const viewEl = document.createElement( + 'tr-ui-a-multi-power-sample-sub-view'); + viewEl.selection = undefined; + this.addHTMLOutput(viewEl); + }); + + test('instantiate_noVSyncs', function() { + const model = new tr.Model(); + const series = new tr.model.PowerSeries(model.device); + + model.device.vSyncTimestamps = []; + series.addPowerSample(1, 1); + series.addPowerSample(2, 2); + series.addPowerSample(3, 3); + series.addPowerSample(4, 2); + + const view = document.createElement('tr-ui-a-multi-power-sample-sub-view'); + const eventSet = new tr.model.EventSet(series.samples); + view.selection = eventSet; + + this.addHTMLOutput(view); + + assert.deepEqual(view.$.chart.samples, eventSet); + assert.sameDeepMembers(view.$.chart.vSyncTimestamps, []); + }); + + test('instantiate', function() { + const model = new tr.Model(); + const series = new tr.model.PowerSeries(model.device); + + model.device.vSyncTimestamps = [0]; + series.addPowerSample(1, 1); + series.addPowerSample(2, 2); + series.addPowerSample(3, 3); + series.addPowerSample(4, 2); + + const view = document.createElement('tr-ui-a-multi-power-sample-sub-view'); + const eventSet = new tr.model.EventSet(series.samples); + view.selection = eventSet; + + this.addHTMLOutput(view); + + assert.deepEqual(view.$.chart.samples, eventSet); + assert.sameDeepMembers(view.$.chart.vSyncTimestamps, [0]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view.html new file mode 100644 index 00000000000..1737894f875 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view.html @@ -0,0 +1,234 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/multi_dimensional_view.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-multi-sample-sub-view'> + <template> + <style> + :host { display: block; } + #control { + background-color: #e6e6e6; + background-image: -webkit-gradient(linear, 0 0, 0 100%, + from(#E5E5E5), to(#D1D1D1)); + flex: 0 0 auto; + overflow-x: auto; + } + #control::-webkit-scrollbar { height: 0px; } + #control { + font-size: 12px; + display: flex; + flex-direction: row; + align-items: stretch; + margin: 1px; + margin-right: 2px; + } + tr-ui-b-table { + font-size: 12px; + } + </style> + <div id="control"> + Sample View Option + </div> + <tr-ui-b-table id="table"> + </tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +(function() { + const MultiDimensionalViewBuilder = tr.b.MultiDimensionalViewBuilder; + + Polymer({ + is: 'tr-ui-a-multi-sample-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.viewOption_ = undefined; + this.selection_ = undefined; + }, + + ready() { + const viewSelector = tr.ui.b.createSelector( + this, 'viewOption', 'tracing.ui.analysis.multi_sample_sub_view', + MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW, + [ + { + label: 'Top-down (Tree)', + value: MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW + }, + { + label: 'Top-down (Heavy)', + value: MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW + }, + { + label: 'Bottom-up (Heavy)', + value: MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW + } + ]); + Polymer.dom(this.$.control).appendChild(viewSelector); + this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + }, + + get selection() { + return this.selection_; + }, + + set selection(selection) { + this.selection_ = selection; + this.updateContents_(); + }, + + get viewOption() { + return this.viewOption_; + }, + + set viewOption(viewOption) { + this.viewOption_ = viewOption; + this.updateContents_(); + }, + + createSamplingSummary_(selection, viewOption) { + const builder = new MultiDimensionalViewBuilder( + 1 /* dimensions */, 1 /* valueCount */); + const samples = selection.filter( + event => event instanceof tr.model.Sample); + + samples.forEach(function(sample) { + builder.addPath([sample.userFriendlyStack.reverse()], + [1], MultiDimensionalViewBuilder.ValueKind.SELF); + }); + + return builder.buildView(viewOption); + }, + + processSampleRows_(rows) { + for (const row of rows) { + let title = row.title[0]; + let results = /(.*) (Deoptimized reason: .*)/.exec(title); + if (results !== null) { + row.deoptReason = results[2]; + title = results[1]; + } + results = /(.*) url: (.*)/.exec(title); + if (results !== null) { + row.functionName = results[1]; + row.url = results[2]; + if (row.functionName === '') { + row.functionName = '(anonymous function)'; + } + if (row.url === '') { + row.url = 'unknown'; + } + } else { + row.functionName = title; + row.url = 'unknown'; + } + this.processSampleRows_(row.subRows); + } + }, + + updateContents_() { + if (this.selection === undefined) { + this.$.table.tableColumns = []; + this.$.table.tableRows = []; + this.$.table.rebuild(); + return; + } + + const samplingData = this.createSamplingSummary_( + this.selection, this.viewOption); + const total = samplingData.values[0].total; + const columns = [ + this.createPercentColumn_('Total', total), + this.createSamplesColumn_('Total'), + this.createPercentColumn_('Self', total), + this.createSamplesColumn_('Self'), + { + title: 'Function Name', + value(row) { + // For function that got deoptimized, show function name + // as red italic with a tooltip + if (row.deoptReason !== undefined) { + const spanEl = tr.ui.b.createSpan({ + italic: true, + color: '#F44336', + tooltip: row.deoptReason + }); + spanEl.innerText = row.functionName; + return spanEl; + } + return row.functionName; + }, + width: '150px', + cmp: (a, b) => a.functionName.localeCompare(b.functionName), + showExpandButtons: true + }, + { + title: 'Location', + value(row) { return row.url; }, + width: '250px', + cmp: (a, b) => a.url.localeCompare(b.url), + } + ]; + + this.processSampleRows_(samplingData.subRows); + this.$.table.tableColumns = columns; + this.$.table.sortColumnIndex = 1; /* Total samples */ + this.$.table.sortDescending = true; + this.$.table.tableRows = samplingData.subRows; + this.$.table.rebuild(); + }, + + createPercentColumn_(title, samplingDataTotal) { + const field = title.toLowerCase(); + return { + title: title + ' percent', + value(row) { + return tr.v.ui.createScalarSpan( + row.values[0][field] / samplingDataTotal, { + customContextRange: tr.b.math.Range.PERCENT_RANGE, + unit: tr.b.Unit.byName.normalizedPercentage, + context: { minimumFractionDigits: 2, maximumFractionDigits: 2 }, + }); + }, + width: '60px', + cmp: (a, b) => a.values[0][field] - b.values[0][field] + }; + }, + + createSamplesColumn_(title) { + const field = title.toLowerCase(); + return { + title: title + ' samples', + value(row) { + return tr.v.ui.createScalarSpan(row.values[0][field], { + unit: tr.b.Unit.byName.unitlessNumber, + context: { maximumFractionDigits: 0 }, + }); + }, + width: '60px', + cmp: (a, b) => a.values[0][field] - b.values[0][field] + }; + } + }); + + tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-multi-sample-sub-view', + tr.model.Sample, + { + multi: true, + title: 'Samples', + }); +})(); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view_test.html new file mode 100644 index 00000000000..e148a2d7c95 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view_test.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/multi_sample_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSampleNamed = tr.c.TestUtils.newSampleNamed; + + function instantiateWithTraces(traces) { + let t53; + const m = tr.c.TestUtils.newModelWithEvents([], { + shiftWorldToZero: false, + pruneContainers: false, + customizeModelCallback(model) { + t53 = model.getOrCreateProcess(52).getOrCreateThread(53); + traces.forEach(function(trace, index) { + model.samples.push( + newSampleNamed(t53, 'X', 'cat', trace, index * 0.02)); + }); + } + }); + + const t53track = {}; + t53track.thread = t53; + + const selection = new tr.model.EventSet(); + for (let i = 0; i < t53.samples.length; i++) { + selection.push(t53.samples[i]); + } + + const view = document.createElement('tr-ui-a-multi-sample-sub-view'); + view.style.height = '500px'; + this.addHTMLOutput(view); + view.selection = selection; + return view; + } + + test('instantiate_flat', function() { + instantiateWithTraces.call(this, [ + ['BBB'], + ['AAA'], + ['AAA'], + ['Sleeping'], + ['BBB'], + ['AAA'], + ['CCC'], + ['Sleeping'] + ]); + }); + + test('instantiate_nested', function() { + instantiateWithTraces.call(this, [ + ['AAA', 'BBB'], + ['AAA', 'BBB', 'CCC'], + ['AAA', 'BBB'], + ['BBB', 'AAA', 'BBB'], + ['BBB', 'AAA', 'BBB'], + ['BBB', 'AAA', 'BBB'] + ]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view.html new file mode 100644 index 00000000000..b896a7bf699 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view.html @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/related_events.html"> + +<dom-module id='tr-ui-a-multi-thread-slice-sub-view'> + <template> + <style> + :host { + display: flex; + } + #content { + display: flex; + flex: 1 1 auto; + min-width: 0; + } + #content > tr-ui-a-related-events { + margin-left: 8px; + flex: 0 1 200px; + } + </style> + <div id="content"></div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-multi-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.selection_ = undefined; + }, + + get selection() { + return this.selection_; + }, + + set selection(selection) { + this.selection_ = selection; + + // TODO(nduca): This is a gross hack for cc Frame Viewer, but its only + // the frame viewer that needs this feature, so ~shrug~. + // We check for its presence so that we do not have a hard dependency + // on frame viewer. + if (tr.isExported('tr.ui.e.chrome.cc.RasterTaskSelection')) { + if (tr.ui.e.chrome.cc.RasterTaskSelection.supports(selection)) { + const ltvSelection = new tr.ui.e.chrome.cc.RasterTaskSelection( + selection); + + const ltv = new tr.ui.e.chrome.cc.LayerTreeHostImplSnapshotView(); + ltv.objectSnapshot = ltvSelection.containingSnapshot; + ltv.selection = ltvSelection; + ltv.extraHighlightsByLayerId = ltvSelection.extraHighlightsByLayerId; + + Polymer.dom(this.$.content).textContent = ''; + Polymer.dom(this.$.content).appendChild(ltv); + + this.requiresTallView_ = true; + return; + } + } + + Polymer.dom(this.$.content).textContent = ''; + + const mesv = document.createElement('tr-ui-a-multi-event-sub-view'); + mesv.selection = selection; + Polymer.dom(this.$.content).appendChild(mesv); + + const relatedEvents = document.createElement('tr-ui-a-related-events'); + relatedEvents.setRelatedEvents(selection); + + if (relatedEvents.hasRelatedEvents()) { + Polymer.dom(this.$.content).appendChild(relatedEvents); + } + }, + + get requiresTallView() { + if (this.$.content.children.length === 0) return false; + const childTagName = this.$.content.children[0].tagName; + if (childTagName === 'TR-UI-A-MULTI-EVENT-SUB-VIEW') { + return false; + } + + // Using raster task view. + return true; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-multi-thread-slice-sub-view', + tr.model.ThreadSlice, + { + multi: true, + title: 'Slices', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view_test.html new file mode 100644 index 00000000000..a44419cf536 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view_test.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/thread_slice.html"> +<link rel="import" href="/tracing/ui/analysis/multi_thread_slice_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + const newFlowEventEx = tr.c.TestUtils.newFlowEventEx; + + test('instantiate', function() { + const model = new tr.Model(); + const t53 = model.getOrCreateProcess(52).getOrCreateThread(53); + t53.sliceGroup.pushSlice( + newSliceEx({title: 'a', start: 0.0, duration: 0.5, + type: tr.model.ThreadSlice})); + t53.sliceGroup.pushSlice( + newSliceEx({title: 'b', start: 1.0, duration: 2, + type: tr.model.ThreadSlice})); + t53.sliceGroup.createSubSlices(); + + const selection = new tr.model.EventSet(); + selection.push(t53.sliceGroup.slices[0]); + selection.push(t53.sliceGroup.slices[1]); + + const viewEl = document.createElement( + 'tr-ui-a-multi-thread-slice-sub-view'); + viewEl.selection = selection; + this.addHTMLOutput(viewEl); + }); + + test('withFlows', function() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + + m.t2 = m.p1.getOrCreateThread(2); + m.t3 = m.p1.getOrCreateThread(3); + m.t4 = m.p1.getOrCreateThread(4); + + m.sA = m.t2.sliceGroup.pushSlice( + newSliceEx({title: 'a', start: 0, end: 5, + type: tr.model.ThreadSlice})); + m.sB = m.t3.sliceGroup.pushSlice( + newSliceEx({title: 'b', start: 10, end: 15, + type: tr.model.ThreadSlice})); + m.sC = m.t4.sliceGroup.pushSlice( + newSliceEx({title: 'c', start: 20, end: 20, + type: tr.model.ThreadSlice})); + + m.t2.createSubSlices(); + m.t3.createSubSlices(); + m.t4.createSubSlices(); + + m.f1 = newFlowEventEx({ + title: 'flowish', start: 0, end: 10, + startSlice: m.sA, + endSlice: m.sB + }); + m.f2 = newFlowEventEx({ + title: 'flowish', start: 15, end: 21, + startSlice: m.sB, + endSlice: m.sC + }); + }); + + const selection = new tr.model.EventSet(); + selection.push(m.sA); + selection.push(m.sB); + selection.push(m.sC); + + const viewEl = document.createElement( + 'tr-ui-a-multi-thread-slice-sub-view'); + viewEl.selection = selection; + this.addHTMLOutput(viewEl); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view.html new file mode 100644 index 00000000000..f1f0666fc43 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html"> + +<dom-module id='tr-ui-a-multi-thread-time-slice-sub-view'> + <template> + <style> + :host { + display: flex; + } + #content { + flex: 1 1 auto; + min-width: 0; + } + </style> + <tr-ui-a-multi-event-sub-view id="content"></tr-ui-a-multi-event-sub-view> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-multi-thread-time-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + ready() { + this.$.content.eventsHaveSubRows = false; + }, + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.content.setSelectionWithoutErrorChecks(selection); + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-multi-thread-time-slice-sub-view', + tr.model.ThreadTimeSlice, + { + multi: true, + title: 'Thread Timeslices', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view_test.html new file mode 100644 index 00000000000..e3489fe1c9c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view_test.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/multi_thread_time_slice_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createBasicModel() { + const lines = [ + 'Android.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck + ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck + 'Android.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck + 'Android.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck + ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck + ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E', + ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck + ]; + + return tr.c.TestUtils.newModelWithEvents([lines.join('\n')], { + shiftWorldToZero: false + }); + } + + test('instantiate', function() { + const m = createBasicModel(); + + const thread = m.findAllThreadsNamed('Binder_1')[0]; + + const selection = new tr.model.EventSet(); + selection.push(thread.timeSlices[0]); + selection.push(thread.timeSlices[1]); + + const viewEl = document.createElement( + 'tr-ui-a-multi-thread-time-slice-sub-view'); + viewEl.selection = selection; + this.addHTMLOutput(viewEl); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_user_expectation_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_user_expectation_sub_view.html new file mode 100644 index 00000000000..b89e3fd0724 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_user_expectation_sub_view.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/user_expectation_related_samples_table.html"> + +<dom-module id='tr-ui-a-multi-user-expectation-sub-view'> + <template> + <style> + :host { + display: flex; + flex: 1 1 auto; + } + #events { + margin-left: 8px; + flex: 0 1 200px; + } + </style> + <tr-ui-a-multi-event-sub-view id="realView"></tr-ui-a-multi-event-sub-view> + <div id="events"> + <tr-ui-a-user-expectation-related-samples-table id="relatedSamples"></tr-ui-a-user-expectation-related-samples-table> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-multi-interaction-record-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.currentSelection_ = undefined; + }, + + set selection(selection) { + this.currentSelection_ = selection; + this.$.realView.setSelectionWithoutErrorChecks(selection); + + this.currentSelection_ = selection; + + this.$.relatedSamples.selection = selection; + if (this.$.relatedSamples.hasRelatedSamples()) { + this.$.events.style.display = ''; + } else { + this.$.events.style.display = 'none'; + } + }, + + get selection() { + return this.currentSelection_; + }, + + get relatedEventsToHighlight() { + if (!this.currentSelection_) return undefined; + + const selection = new tr.model.EventSet(); + this.currentSelection_.forEach(function(ir) { + ir.associatedEvents.forEach(function(event) { + selection.push(event); + }); + }); + return selection; + } +}); +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-user-expectation-sub-view', + tr.model.um.UserExpectation, + { + multi: true, + title: 'User Expectations', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_instance_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_instance_view.html new file mode 100644 index 00000000000..3c76dc8c9e3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_instance_view.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/extension_registry.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const ObjectInstanceView = tr.ui.b.define('object-instance-view'); + + ObjectInstanceView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.objectInstance_ = undefined; + }, + + get requiresTallView() { + return true; + }, + + set modelEvent(obj) { + this.objectInstance = obj; + }, + + get modelEvent() { + return this.objectInstance; + }, + + get objectInstance() { + return this.objectInstance_; + }, + + set objectInstance(i) { + this.objectInstance_ = i; + this.updateContents(); + }, + + updateContents() { + throw new Error('Not implemented'); + } + }; + + const options = new tr.b.ExtensionRegistryOptions( + tr.b.TYPE_BASED_REGISTRY_MODE); + options.mandatoryBaseClass = ObjectInstanceView; + options.defaultMetadata = { + showInTrackView: true + }; + tr.b.decorateExtensionRegistry(ObjectInstanceView, options); + + return { + ObjectInstanceView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_snapshot_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_snapshot_view.html new file mode 100644 index 00000000000..a42ed0ec02b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_snapshot_view.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/extension_registry.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const ObjectSnapshotView = tr.ui.b.define('object-snapshot-view'); + + ObjectSnapshotView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.objectSnapshot_ = undefined; + }, + + get requiresTallView() { + return true; + }, + + set modelEvent(obj) { + this.objectSnapshot = obj; + }, + + get modelEvent() { + return this.objectSnapshot; + }, + + get objectSnapshot() { + return this.objectSnapshot_; + }, + + set objectSnapshot(i) { + this.objectSnapshot_ = i; + this.updateContents(); + }, + + updateContents() { + throw new Error('Not implemented'); + } + }; + + const options = new tr.b.ExtensionRegistryOptions( + tr.b.TYPE_BASED_REGISTRY_MODE); + options.mandatoryBaseClass = ObjectSnapshotView; + options.defaultMetadata = { + showInstances: true, + showInTrackView: true + }; + tr.b.decorateExtensionRegistry(ObjectSnapshotView, options); + + return { + ObjectSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table.html new file mode 100644 index 00000000000..337e4f5ba56 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table.html @@ -0,0 +1,135 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/unit_scale.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-a-power-sample-summary-table'> + <template> + <style> + tr-ui-b-table { + font-size: 12px; + } + </style> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-power-sample-summary-table', + + ready() { + this.$.table.tableColumns = [ + { + title: 'Min power', + width: '100px', + value(row) { + return tr.b.Unit.byName.powerInWatts.format(row.min); + } + }, + { + title: 'Max power', + width: '100px', + value(row) { + return tr.b.Unit.byName.powerInWatts.format(row.max); + } + }, + { + title: 'Time-weighted average', + width: '100px', + value(row) { + return tr.b.Unit.byName.powerInWatts.format( + row.timeWeightedAverageInW); + } + }, + { + title: 'Energy consumed', + width: '100px', + value(row) { + return tr.b.Unit.byName.energyInJoules.format(row.energyConsumedInJ); + } + }, + { + title: 'Sample count', + width: '100%', + value(row) { return row.sampleCount; } + } + ]; + this.samples = new tr.model.EventSet(); + }, + + get samples() { + return this.samples_; + }, + + set samples(samples) { + if (samples === this.samples) return; + + this.samples_ = + (samples === undefined) ? new tr.model.EventSet() : samples; + this.updateContents_(); + }, + + updateContents_() { + if (this.samples.length === 0) { + this.$.table.tableRows = []; + } else { + this.$.table.tableRows = [{ + min: this.getMin(), + max: this.getMax(), + timeWeightedAverageInW: this.getTimeWeightedAverageInW(), + energyConsumedInJ: this.getEnergyConsumedInJ(), + sampleCount: this.samples.length + }]; + } + + this.$.table.rebuild(); + }, + + getMin() { + return Math.min.apply(null, this.samples.map(function(sample) { + return sample.powerInW; + })); + }, + + getMax() { + return Math.max.apply(null, this.samples.map(function(sample) { + return sample.powerInW; + })); + }, + + /** + * Returns a time-weighted average of the power consumption (Watts) + * in between the first sample (inclusive) and last sample (exclusive). + */ + getTimeWeightedAverageInW() { + const energyConsumedInJ = this.getEnergyConsumedInJ(); + + if (energyConsumedInJ === 'N/A') return 'N/A'; + + const durationInS = tr.b.convertUnit(this.samples.bounds.duration, + tr.b.UnitPrefixScale.METRIC.MILLI, + tr.b.UnitPrefixScale.METRIC.NONE); + + return energyConsumedInJ / durationInS; + }, + + + getEnergyConsumedInJ() { + if (this.samples.length < 2) return 'N/A'; + + const bounds = this.samples.bounds; + const series = tr.b.getFirstElement(this.samples).series; + return series.getEnergyConsumedInJ(bounds.min, bounds.max); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table_test.html new file mode 100644 index 00000000000..f5bf8c7d5a0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table_test.html @@ -0,0 +1,137 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/power_series.html"> +<link rel="import" href="/tracing/ui/analysis/power_sample_summary_table.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const EventSet = tr.model.EventSet; + const Model = tr.Model; + const PowerSeries = tr.model.PowerSeries; + + test('instantiate', function() { + const series = new PowerSeries(new Model().device); + + series.addPowerSample(0, 1); + series.addPowerSample(1000, 2); + series.addPowerSample(2000, 3); + series.addPowerSample(3000, 4); + + const table = document.createElement('tr-ui-a-power-sample-summary-table'); + table.samples = new EventSet(series.samples); + + this.addHTMLOutput(table); + }); + + test('setSamples_undefinedPowerSamples', function() { + const table = document.createElement('tr-ui-a-power-sample-summary-table'); + table.samples = undefined; + + assert.lengthOf(table.$.table.tableRows, 0); + }); + + test('setSamples_noPowerSamples', function() { + const table = document.createElement('tr-ui-a-power-sample-summary-table'); + table.samples = new EventSet([]); + + assert.lengthOf(table.$.table.tableRows, 0); + }); + + test('setSamples_onePowerSample', function() { + const series = new PowerSeries(new Model().device); + + series.addPowerSample(0, 1); + + const table = document.createElement('tr-ui-a-power-sample-summary-table'); + table.samples = new EventSet(series.samples); + + assert.lengthOf(table.$.table.tableRows, 1); + assert.strictEqual(table.$.table.tableRows[0].min, 1); + assert.strictEqual(table.$.table.tableRows[0].max, 1); + assert.strictEqual( + table.$.table.tableRows[0].timeWeightedAverageInW, 'N/A'); + assert.strictEqual(table.$.table.tableRows[0].energyConsumedInJ, 'N/A'); + assert.strictEqual(table.$.table.tableRows[0].sampleCount, 1); + }); + + test('setSamples_twoPowerSamples', function() { + const series = new PowerSeries(new Model().device); + + series.addPowerSample(0, 1); + series.addPowerSample(1000, 2); + + const table = document.createElement('tr-ui-a-power-sample-summary-table'); + table.samples = new EventSet(series.samples); + + assert.lengthOf(table.$.table.tableRows, 1); + assert.strictEqual(table.$.table.tableRows[0].min, 1); + assert.strictEqual(table.$.table.tableRows[0].max, 2); + assert.strictEqual(table.$.table.tableRows[0].timeWeightedAverageInW, 1); + assert.strictEqual(table.$.table.tableRows[0].energyConsumedInJ, 1); + assert.strictEqual(table.$.table.tableRows[0].sampleCount, 2); + }); + + test('setSamples_threePowerSamples', function() { + const series = new PowerSeries(new Model().device); + + series.addPowerSample(0, 1); + series.addPowerSample(1000, 2); + series.addPowerSample(2000, 3); + + const table = document.createElement('tr-ui-a-power-sample-summary-table'); + table.samples = new EventSet(series.samples); + + assert.lengthOf(table.$.table.tableRows, 1); + assert.strictEqual(table.$.table.tableRows[0].min, 1); + assert.strictEqual(table.$.table.tableRows[0].max, 3); + assert.strictEqual(table.$.table.tableRows[0].timeWeightedAverageInW, 1.5); + assert.strictEqual(table.$.table.tableRows[0].energyConsumedInJ, 3); + assert.strictEqual(table.$.table.tableRows[0].sampleCount, 3); + }); + + test('setSamples_columnsInitialized', function() { + const series = new PowerSeries(new Model().device); + + series.addPowerSample(0, 1); + series.addPowerSample(1000, 2); + series.addPowerSample(2000, 3); + + const table = document.createElement('tr-ui-a-power-sample-summary-table'); + table.samples = new EventSet(series.samples); + + const row = table.$.table.tableRows[0]; + const columns = table.$.table.tableColumns; + + assert.lengthOf(columns, 5); + + assert.strictEqual(columns[0].title, 'Min power'); + assert.strictEqual(columns[0].width, '100px'); + assert.strictEqual(columns[0].value(row), '1.000 W'); + + assert.strictEqual(columns[1].title, 'Max power'); + assert.strictEqual(columns[1].width, '100px'); + assert.strictEqual(columns[1].value(row), '3.000 W'); + + assert.strictEqual(columns[2].title, 'Time-weighted average'); + assert.strictEqual(columns[2].width, '100px'); + assert.strictEqual(columns[2].value(row), '1.500 W'); + + assert.strictEqual(columns[3].title, 'Energy consumed'); + assert.strictEqual(columns[3].width, '100px'); + assert.strictEqual(columns[3].value(row), '3.000 J'); + + assert.strictEqual(columns[4].title, 'Sample count'); + assert.strictEqual(columns[4].width, '100%'); + assert.strictEqual(columns[4].value(row), 3); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior.html new file mode 100644 index 00000000000..62abbe8076b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/raf.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const RebuildableBehavior = { + rebuild() { + /** + * Rebuild the pane if necessary. + * + * This method is not intended to be overriden by subclasses. Please + * override scheduleRebuild_() instead. + */ + if (!this.paneDirty_) { + // Avoid rebuilding unnecessarily as it breaks things like table + // selection. + return; + } + + this.paneDirty_ = false; + this.onRebuild_(); + }, + + /** + * Mark the UI state of the pane as dirty and schedule a rebuild. + * + * This method is intended to be called by subclasses. + */ + scheduleRebuild_() { + if (this.paneDirty_) return; + this.paneDirty_ = true; + tr.b.requestAnimationFrame(this.rebuild.bind(this)); + }, + + /** + * Called when the pane is dirty and a rebuild is triggered. + * + * This method is intended to be overriden by subclasses (instead of + * directly overriding rebuild()). + */ + onRebuild_() { + } + }; + + return { + RebuildableBehavior, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior_test.html new file mode 100644 index 00000000000..2a2f083ceb3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior_test.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/rebuildable_behavior.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + Polymer({ + is: 'tr-ui-analysis-rebuildable-test-element', + behaviors: [tr.ui.analysis.RebuildableBehavior] + }); + + test('rebuild', function() { + const el = document.createElement( + 'tr-ui-analysis-rebuildable-test-element'); + let didFireOnRebuild; + el.onRebuild_ = function() { + assert.strictEqual(this, el); + didFireOnRebuild = true; + }; + + function checkManualRebuild(expectedDidFireOnRebuild) { + didFireOnRebuild = false; + el.rebuild(); + assert.strictEqual(didFireOnRebuild, expectedDidFireOnRebuild); + } + + function checkRAFRebuild(expectedDidFireOnRebuild) { + didFireOnRebuild = false; + tr.b.forcePendingRAFTasksToRun(); + assert.strictEqual(didFireOnRebuild, expectedDidFireOnRebuild); + } + + // No rebuilds should occur when not scheduled. + checkManualRebuild(false); + checkRAFRebuild(false); + + // Single rebuild should occur when scheduled once. + el.scheduleRebuild_(); + checkManualRebuild(true); + checkManualRebuild(false); + + el.scheduleRebuild_(); + checkRAFRebuild(true); + checkRAFRebuild(false); + + // Only a single rebuild should occur even when scheduled multiple times. + el.scheduleRebuild_(); + el.scheduleRebuild_(); + checkManualRebuild(true); + checkRAFRebuild(false); + checkManualRebuild(false); + + el.scheduleRebuild_(); + el.scheduleRebuild_(); + checkRAFRebuild(true); + checkRAFRebuild(false); + checkManualRebuild(false); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events.html new file mode 100644 index 00000000000..b4036837bf5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events.html @@ -0,0 +1,354 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/task.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/analysis/flow_classifier.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-a-related-events'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + #table { + flex: 1 1 auto; + align-self: stretch; + font-size: 12px; + } + </style> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +function* getEventInFlowEvents(event) { + if (!event.inFlowEvents) return; + yield* event.inFlowEvents; +} + +function* getEventOutFlowEvents(event) { + if (!event.outFlowEvents) return; + yield* event.outFlowEvents; +} + +function* getEventAncestors(event) { + if (!event.enumerateAllAncestors) return; + yield* event.enumerateAllAncestors(); +} + +function* getEventDescendents(event) { + if (!event.enumerateAllDescendents) return; + yield* event.enumerateAllDescendents(); +} + +Polymer({ + is: 'tr-ui-a-related-events', + + ready() { + this.eventGroups_ = []; + this.cancelFunctions_ = []; + + this.$.table.tableColumns = [ + { + title: 'Event(s)', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.type; + if (row.tooltip) { + typeEl.title = row.tooltip; + } + return typeEl; + }, + width: '150px' + }, + { + title: 'Link', + width: '100%', + value(row) { + const linkEl = document.createElement('tr-ui-a-analysis-link'); + if (row.name) { + linkEl.setSelectionAndContent(row.selection, row.name); + } else { + linkEl.selection = row.selection; + } + return linkEl; + } + } + ]; + }, + + hasRelatedEvents() { + return (this.eventGroups_ && this.eventGroups_.length > 0); + }, + + setRelatedEvents(eventSet) { + this.cancelAllTasks_(); + this.eventGroups_ = []; + this.addRuntimeCallStats_(eventSet); + this.addOverlappingV8ICStats_(eventSet); + this.addV8GCObjectStats_(eventSet); + this.addV8Slices_(eventSet); + this.addConnectedFlows_(eventSet); + this.addConnectedEvents_(eventSet); + this.addOverlappingSamples_(eventSet); + this.updateContents_(); + }, + + addConnectedFlows_(eventSet) { + const classifier = new tr.ui.analysis.FlowClassifier(); + eventSet.forEach(function(slice) { + if (slice.inFlowEvents) { + slice.inFlowEvents.forEach(function(flow) { + classifier.addInFlow(flow); + }); + } + if (slice.outFlowEvents) { + slice.outFlowEvents.forEach(function(flow) { + classifier.addOutFlow(flow); + }); + } + }); + if (!classifier.hasEvents()) return; + + const addToEventGroups = function(type, flowEvent) { + this.eventGroups_.push({ + type, + selection: new tr.model.EventSet(flowEvent), + name: flowEvent.title + }); + }; + + classifier.inFlowEvents.forEach( + addToEventGroups.bind(this, 'Incoming flow')); + classifier.outFlowEvents.forEach( + addToEventGroups.bind(this, 'Outgoing flow')); + classifier.internalFlowEvents.forEach( + addToEventGroups.bind(this, 'Internal flow')); + }, + + cancelAllTasks_() { + this.cancelFunctions_.forEach(function(cancelFunction) { + cancelFunction(); + }); + this.cancelFunctions_ = []; + }, + + addConnectedEvents_(eventSet) { + this.cancelFunctions_.push(this.createEventsLinkIfNeeded_( + 'Preceding events', + 'Add all events that have led to the selected one(s), connected by ' + + 'flow arrows or by call stack.', + eventSet, + function* (event) { + yield* getEventInFlowEvents(event); + yield* getEventAncestors(event); + if (event.startSlice) { + yield event.startSlice; + } + }.bind(this))); + this.cancelFunctions_.push(this.createEventsLinkIfNeeded_( + 'Following events', + 'Add all events that have been caused by the selected one(s), ' + + 'connected by flow arrows or by call stack.', + eventSet, + function* (event) { + yield* getEventOutFlowEvents(event); + yield* getEventDescendents(event); + if (event.endSlice) { + yield event.endSlice; + } + }.bind(this))); + this.cancelFunctions_.push(this.createEventsLinkIfNeeded_( + 'All connected events', + 'Add all events connected to the selected one(s) by flow arrows or ' + + 'by call stack.', + eventSet, + function* (event) { + yield* getEventInFlowEvents(event); + yield* getEventOutFlowEvents(event); + yield* getEventAncestors(event); + yield* getEventDescendents(event); + if (event.startSlice) { + yield event.startSlice; + } + if (event.endSlice) { + yield event.endSlice; + } + }.bind(this))); + }, + + createEventsLinkIfNeeded_(title, tooltip, events, connectedFn) { + events = new tr.model.EventSet(events); + const eventsToProcess = new Set(events); + // for (let event of events) + // eventsToProcess.add(event); + let wasChanged = false; + let task; + let isCanceled = false; + function addEventsUntilTimeout() { + if (isCanceled) return; + // Let's grant ourselves a budget of 8 ms. If time runs out, then + // create another task to do the rest. + const timeout = window.performance.now() + 8; + // TODO(alexandermont): Don't check window.performance.now + // every iteration. + while (eventsToProcess.size > 0 && + window.performance.now() <= timeout) { + // Get the next event. + const nextEvent = tr.b.getFirstElement(eventsToProcess); + eventsToProcess.delete(nextEvent); + + // Add the connected events to the list. + for (const eventToAdd of connectedFn(nextEvent)) { + if (!events.contains(eventToAdd)) { + events.push(eventToAdd); + eventsToProcess.add(eventToAdd); + wasChanged = true; + } + } + } + if (eventsToProcess.size > 0) { + // There are still events to process, but we ran out of time. Post + // more work for later. + const newTask = new tr.b.Task( + addEventsUntilTimeout.bind(this), this); + task.after(newTask); + task = newTask; + return; + } + // Went through all events, add the link. + if (!wasChanged) return; + this.eventGroups_.push({ + type: title, + tooltip, + selection: events + }); + this.updateContents_(); + } + function cancelTask() { + isCanceled = true; + } + task = new tr.b.Task(addEventsUntilTimeout.bind(this), this); + tr.b.Task.RunWhenIdle(task); + return cancelTask; + }, + + addOverlappingSamples_(eventSet) { + const samples = new tr.model.EventSet(); + for (const slice of eventSet) { + if (!slice.parentContainer || !slice.parentContainer.samples) { + continue; + } + const candidates = slice.parentContainer.samples; + const range = tr.b.math.Range.fromExplicitRange( + slice.start, slice.start + slice.duration); + const filteredSamples = range.filterArray( + candidates, function(value) {return value.start;}); + for (const sample of filteredSamples) { + samples.push(sample); + } + } + if (samples.length > 0) { + this.eventGroups_.push({ + type: 'Overlapping samples', + tooltip: 'All samples overlapping the selected slice(s).', + selection: samples + }); + } + }, + + addV8Slices_(eventSet) { + const v8Slices = new tr.model.EventSet(); + for (const slice of eventSet) { + if (slice.category === 'v8') { + v8Slices.push(slice); + } + } + if (v8Slices.length > 0) { + this.eventGroups_.push({ + type: 'V8 Slices', + tooltip: 'All V8 slices in the selected slice(s).', + selection: v8Slices + }); + } + }, + + addRuntimeCallStats_(eventSet) { + const slices = eventSet.filter(function(slice) { + return (slice.category === 'v8' || + slice.category === 'disabled-by-default-v8.runtime_stats') && + slice.runtimeCallStats; + }); + if (slices.length > 0) { + this.eventGroups_.push({ + type: 'Runtime call stats table', + // eslint-disable-next-line + tooltip: 'All V8 slices containing runtime call stats table in the selected slice(s).', + selection: slices + }); + } + }, + + addV8GCObjectStats_(eventSet) { + const slices = new tr.model.EventSet(); + for (const slice of eventSet) { + if (slice.title === 'V8.GC_Objects_Stats') { + slices.push(slice); + } + } + if (slices.length > 0) { + this.eventGroups_.push({ + type: 'V8 GC stats table', + tooltip: 'All V8 GC statistics slices in the selected set.', + selection: slices + }); + } + }, + + addOverlappingV8ICStats_(eventSet) { + const slices = new tr.model.EventSet(); + for (const slice of eventSet) { + if (!slice.parentContainer || !slice.parentContainer.sliceGroup) { + continue; + } + const sliceGroup = slice.parentContainer.sliceGroup.slices; + const range = tr.b.math.Range.fromExplicitRange( + slice.start, slice.start + slice.duration); + const filteredSlices = range.filterArray( + sliceGroup, value => value.start); + const icSlices = filteredSlices.filter(x => x.title === 'V8.ICStats'); + for (const icSlice of icSlices) { + slices.push(icSlice); + } + } + if (slices.length > 0) { + this.eventGroups_.push({ + type: 'Overlapping V8 IC stats', + tooltip: 'All V8 IC statistics overlapping the selected set.', + selection: slices + }); + } + }, + + updateContents_() { + const table = this.$.table; + if (this.eventGroups_ === undefined) { + table.tableRows = []; + } else { + table.tableRows = this.eventGroups_.slice(); + } + table.rebuild(); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events_test.html new file mode 100644 index 00000000000..5d14d68faa3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events_test.html @@ -0,0 +1,221 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/sample.html"> +<link rel="import" href="/tracing/model/thread_slice.html"> +<link rel="import" href="/tracing/ui/analysis/related_events.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + const newFlowEventEx = tr.c.TestUtils.newFlowEventEx; + + function createModel() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + + m.t2 = m.p1.getOrCreateThread(2); + m.t3 = m.p1.getOrCreateThread(3); + m.t4 = m.p1.getOrCreateThread(4); + const node = tr.c.TestUtils.newProfileNodes(m, ['fake']); + + // Setup samples and slices in this way: + // 0 5 10 15 20 + // _____________________________ + // t2 * + // [ a ][ ]aa + // ----------------------------- + // t3 * * * * * + // * * + // [ b ] + // [bb] + // []bbb + // ----------------------------- + // t4 |c + // ----------------------------- + m.samples.push( + new tr.model.Sample(10, 'b10_1', node, m.t3), + new tr.model.Sample(7, 'b7', node, m.t3), + new tr.model.Sample(12, 'b12', node, m.t3), + new tr.model.Sample(20, 'b20', node, m.t3), + new tr.model.Sample(10, 'b10_2', node, m.t3), + new tr.model.Sample(15, 'b15_1', node, m.t3), + new tr.model.Sample(15, 'b15_2', node, m.t3), + new tr.model.Sample(12, 'a12', node, m.t2) + ); + + m.sA = m.t2.sliceGroup.pushSlice( + newSliceEx({title: 'a', start: 0, end: 5, + type: tr.model.ThreadSlice})); + m.sAA = m.t2.sliceGroup.pushSlice( + newSliceEx({title: 'aa', start: 6, end: 8, + type: tr.model.ThreadSlice})); + m.sB = m.t3.sliceGroup.pushSlice( + newSliceEx({title: 'b', start: 10, end: 15, + type: tr.model.ThreadSlice})); + m.sBB = m.t3.sliceGroup.pushSlice( + newSliceEx({title: 'bb', start: 11, end: 14, + type: tr.model.ThreadSlice})); + m.sBBB = m.t3.sliceGroup.pushSlice( + newSliceEx({title: 'bbb', start: 12, end: 13, + type: tr.model.ThreadSlice})); + m.sC = m.t4.sliceGroup.pushSlice( + newSliceEx({title: 'c', start: 20, end: 20, + type: tr.model.ThreadSlice})); + + m.t2.createSubSlices(); + m.t3.createSubSlices(); + m.t4.createSubSlices(); + + // Add flow events. + m.f0 = newFlowEventEx({ + title: 'a_aa', start: 5, end: 6, + startSlice: m.sA, + endSlice: m.sAA + }); + m.f1 = newFlowEventEx({ + title: 'a_b', start: 0, end: 10, + startSlice: m.sA, + endSlice: m.sB + }); + m.f2 = newFlowEventEx({ + title: 'b_bbb', start: 10, end: 12, + startSlice: m.sB, + endSlice: m.sBBB + }); + m.f3 = newFlowEventEx({ + title: 'bbb_c', start: 13, end: 20, + startSlice: m.sBBB, + endSlice: m.sC + }); + }); + return m; + } + + test('instantiate', function() { + const m = createModel(); + + const viewEl = document.createElement('tr-ui-a-related-events'); + const selection = new tr.model.EventSet( + [m.sA, m.f0, m.sAA, m.f1, m.sB, m.f2, m.sBB, m.sBBB, m.f3, m.sC]); + viewEl.setRelatedEvents(selection); + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + + // Check that the element handles multiple setRelatedEvents calls correctly. + assert.lengthOf(viewEl.$.table.tableRows, 5); + viewEl.setRelatedEvents(selection); + assert.lengthOf(viewEl.$.table.tableRows, 5); + }); + + test('validateFlows', function() { + const m = createModel(); + + const viewEl = document.createElement('tr-ui-a-related-events'); + viewEl.setRelatedEvents(new tr.model.EventSet([m.sB, m.sBB, m.sBBB])); + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + + let inFlows; + let outFlows; + let internalFlows; + viewEl.$.table.tableRows.forEach(function(row) { + if (row.type === 'Incoming flow') { + assert.isUndefined(inFlows); + inFlows = row.selection; + } + if (row.type === 'Outgoing flow') { + assert.isUndefined(outFlows); + outFlows = row.selection; + } + if (row.type === 'Internal flow') { + assert.isUndefined(internalFlows); + internalFlows = row.selection; + } + }); + assert.strictEqual(inFlows.length, 1); + assert.strictEqual(tr.b.getOnlyElement(inFlows).title, 'a_b'); + assert.strictEqual(outFlows.length, 1); + assert.strictEqual(tr.b.getOnlyElement(outFlows).title, 'bbb_c'); + assert.strictEqual(internalFlows.length, 1); + assert.strictEqual(tr.b.getOnlyElement(internalFlows).title, 'b_bbb'); + }); + + test('validateConnectedEvents', function() { + const m = createModel(); + + const viewEl = document.createElement('tr-ui-a-related-events'); + viewEl.setRelatedEvents(new tr.model.EventSet([m.sBB])); + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + + let precedingEvents; + let followingEvents; + let allEvents; + viewEl.$.table.tableRows.forEach(function(row) { + if (row.type === 'Preceding events') { + assert.isUndefined(precedingEvents); + precedingEvents = row.selection; + } + if (row.type === 'Following events') { + assert.isUndefined(followingEvents); + followingEvents = row.selection; + } + if (row.type === 'All connected events') { + assert.isUndefined(allEvents); + allEvents = row.selection; + } + }); + + const precedingTitles = precedingEvents.map(function(e) { + return e.title; + }); + assert.sameMembers(precedingTitles, ['a', 'a_b', 'b', 'bb']); + + const followingTitles = followingEvents.map(function(e) { + return e.title; + }); + assert.sameMembers(followingTitles, ['bb', 'bbb', 'bbb_c', 'c']); + + const allTitles = allEvents.map(function(e) { + return e.title; + }); + assert.sameMembers(allTitles, + ['a', 'a_aa', 'aa', 'a_b', 'b', 'bb', 'bbb', 'b_bbb', 'bbb_c', 'c']); + }); + + test('validateOverlappingSamples', function() { + const m = createModel(); + + const viewEl = document.createElement('tr-ui-a-related-events'); + viewEl.setRelatedEvents(new tr.model.EventSet([m.sB])); + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + + let overlappingSamples; + viewEl.$.table.tableRows.forEach(function(row) { + if (row.type === 'Overlapping samples') { + assert.isUndefined(overlappingSamples); + overlappingSamples = row.selection; + } + }); + + const samplesTitles = overlappingSamples.map(function(e) { + return e.title; + }); + assert.sameMembers(samplesTitles, + ['b10_1', 'b10_2', 'b12', 'b15_1', 'b15_2']); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table.html new file mode 100644 index 00000000000..68ca4d533d4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-selection-summary-table'> + <template> + <style> + :host { + display: flex; + } + #table { + flex: 1 1 auto; + align-self: stretch; + font-size: 12px; + } + </style> + <tr-ui-b-table id="table"> + </tr-ui-b-table> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-selection-summary-table', + created() { + this.selection_ = new tr.b.math.Range(); + }, + + ready() { + this.$.table.showHeader = false; + this.$.table.tableColumns = [ + { + title: 'Name', + value(row) { return row.title; }, + width: '350px' + }, + { + title: 'Value', + width: '100%', + value(row) { + return row.value; + } + } + ]; + }, + + get selection() { + return this.selection_; + }, + + set selection(selection) { + this.selection_ = selection; + this.updateContents_(); + }, + + updateContents_() { + const selection = this.selection_; + const rows = []; + let hasRange; + if (this.selection_ && (!selection.bounds.isEmpty)) { + hasRange = true; + } else { + hasRange = false; + } + + rows.push({ + title: 'Selection start', + value: hasRange ? tr.v.ui.createScalarSpan( + selection.bounds.min, { + unit: tr.b.Unit.byName.timeStampInMs, + ownerDocument: this.ownerDocument + }) : '<empty>' + }); + rows.push({ + title: 'Selection extent', + value: hasRange ? tr.v.ui.createScalarSpan( + selection.bounds.range, { + unit: tr.b.Unit.byName.timeDurationInMs, + ownerDocument: this.ownerDocument + }) : '<empty>' + }); + + this.$.table.tableRows = rows; + this.$.table.rebuild(); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table_test.html new file mode 100644 index 00000000000..20a8daf3b47 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table_test.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/selection_summary_table.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const EventSet = tr.model.EventSet; + const newSliceEx = tr.c.TestUtils.newSliceEx; + + test('noSelection', function() { + const summaryTable = + document.createElement('tr-ui-a-selection-summary-table'); + summaryTable.selection = undefined; + this.addHTMLOutput(summaryTable); + + const tableEl = tr.ui.b.findDeepElementMatching( + summaryTable, 'tr-ui-b-table'); + assert.strictEqual(tableEl.tableRows[0].value, '<empty>'); + assert.strictEqual(tableEl.tableRows[1].value, '<empty>'); + }); + + test('emptySelection', function() { + const summaryTable = + document.createElement('tr-ui-a-selection-summary-table'); + const selection = new EventSet(); + summaryTable.selection = selection; + this.addHTMLOutput(summaryTable); + + const tableEl = tr.ui.b.findDeepElementMatching( + summaryTable, 'tr-ui-b-table'); + assert.strictEqual(tableEl.tableRows[0].value, '<empty>'); + assert.strictEqual(tableEl.tableRows[1].value, '<empty>'); + }); + + test('selection', function() { + const model = new Model(); + const thread = model.getOrCreateProcess(1).getOrCreateThread(2); + const tsg = thread.sliceGroup; + + tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3})); + tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2})); + + const selection = new EventSet(); + selection.push(tsg.slices[0]); + selection.push(tsg.slices[1]); + + const summaryTable = + document.createElement('tr-ui-a-selection-summary-table'); + summaryTable.selection = selection; + this.addHTMLOutput(summaryTable); + + const tableEl = tr.ui.b.findDeepElementMatching( + summaryTable, 'tr-ui-b-table'); + assert.strictEqual(tableEl.tableRows[0].value.value, 0); + assert.strictEqual(tableEl.tableRows[0].value.unit, + tr.b.Unit.byName.timeStampInMs); + assert.strictEqual(tableEl.tableRows[1].value.value, 3); + assert.strictEqual(tableEl.tableRows[1].value.unit, + tr.b.Unit.byName.timeDurationInMs); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view.html new file mode 100644 index 00000000000..cc7f7b840dd --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/related_events.html"> +<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html"> + +<dom-module id='tr-ui-a-single-async-slice-sub-view'> + <template> + <style> + :host { + display: flex; + flex-direction: row; + } + #events { + display:flex; + flex-direction: column; + } + </style> + <tr-ui-a-single-event-sub-view id="content"></tr-ui-a-single-event-sub-view> + <div id="events"> + <tr-ui-a-related-events id="relatedEvents"></tr-ui-a-related-events> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-single-async-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + if (selection.length !== 1) { + throw new Error('Only supports single slices'); + } + this.$.content.setSelectionWithoutErrorChecks(selection); + this.$.relatedEvents.setRelatedEvents(selection); + if (this.$.relatedEvents.hasRelatedEvents()) { + this.$.relatedEvents.style.display = ''; + } else { + this.$.relatedEvents.style.display = 'none'; + } + }, + + getEventRows_(event) { + // TODO(nduca): Figure out if there is a cleaner way to do this. + const rows = this.__proto__.__proto__.getEventRows_(event); + + // Put the ID up top. + rows.splice(0, 0, { + name: 'ID', + value: event.id + }); + return rows; + }, + + get relatedEventsToHighlight() { + if (!this.currentSelection_) return undefined; + return tr.b.getOnlyElement(this.currentSelection_).associatedEvents; + } +}); +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-async-slice-sub-view', + tr.model.AsyncSlice, + { + multi: false, + title: 'Async Slice', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view_test.html new file mode 100644 index 00000000000..6cd341011bb --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view_test.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/single_async_slice_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx; + + test('instantiate', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.asyncSliceGroup.push(newAsyncSliceEx({ + id: 31415, + title: 'a', + start: 10, + duration: 20, + startThread: t1, + endThread: t1 + })); + + const selection = new tr.model.EventSet(); + selection.push(t1.asyncSliceGroup.slices[0]); + + const viewEl = document.createElement( + 'tr-ui-a-single-async-slice-sub-view'); + viewEl.selection = selection; + this.addHTMLOutput(viewEl); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view.html new file mode 100644 index 00000000000..12040ca9bad --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view.html @@ -0,0 +1,149 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-single-cpu-slice-sub-view'> + <template> + <style> + table { + border-collapse: collapse; + border-width: 0; + margin-bottom: 25px; + width: 100%; + } + + table tr > td:first-child { + padding-left: 2px; + } + + table tr > td { + padding: 2px 4px 2px 4px; + vertical-align: text-top; + width: 150px; + } + + table td td { + padding: 0 0 0 0; + width: auto; + } + tr { + vertical-align: top; + } + + tr:nth-child(2n+0) { + background-color: #e2e2e2; + } + </style> + <table> + <tr> + <td>Running process:</td><td id="process-name"></td> + </tr> + <tr> + <td>Running thread:</td><td id="thread-name"></td> + </tr> + <tr> + <td>Start:</td> + <td> + <tr-v-ui-scalar-span id="start"> + </tr-v-ui-scalar-span> + </td> + </tr> + <tr> + <td>Duration:</td> + <td> + <tr-v-ui-scalar-span id="duration"> + </tr-v-ui-scalar-span> + </td> + </tr> + <tr> + <td>Active slices:</td><td id="running-thread"></td> + </tr> + <tr> + <td>Args:</td> + <td> + <tr-ui-a-generic-object-view id="args"> + </tr-ui-a-generic-object-view> + </td> + </tr> + </table> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-single-cpu-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.currentSelection_ = undefined; + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + const cpuSlice = tr.b.getOnlyElement(selection); + if (!(cpuSlice instanceof tr.model.CpuSlice)) { + throw new Error('Only supports thread time slices'); + } + + this.currentSelection_ = selection; + + const thread = cpuSlice.threadThatWasRunning; + + const root = Polymer.dom(this.root); + if (thread) { + Polymer.dom(root.querySelector('#process-name')).textContent = + thread.parent.userFriendlyName; + Polymer.dom(root.querySelector('#thread-name')).textContent = + thread.userFriendlyName; + } else { + root.querySelector('#process-name').parentElement.style.display = + 'none'; + Polymer.dom(root.querySelector('#thread-name')).textContent = + cpuSlice.title; + } + + root.querySelector('#start').setValueAndUnit( + cpuSlice.start, tr.b.Unit.byName.timeStampInMs); + root.querySelector('#duration').setValueAndUnit( + cpuSlice.duration, tr.b.Unit.byName.timeDurationInMs); + + const runningThreadEl = root.querySelector('#running-thread'); + + const timeSlice = cpuSlice.getAssociatedTimeslice(); + if (!timeSlice) { + runningThreadEl.parentElement.style.display = 'none'; + } else { + const threadLink = document.createElement('tr-ui-a-analysis-link'); + threadLink.selection = new tr.model.EventSet(timeSlice); + Polymer.dom(threadLink).textContent = 'Click to select'; + runningThreadEl.parentElement.style.display = ''; + Polymer.dom(runningThreadEl).textContent = ''; + Polymer.dom(runningThreadEl).appendChild(threadLink); + } + + root.querySelector('#args').object = cpuSlice.args; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-cpu-slice-sub-view', + tr.model.CpuSlice, + { + multi: false, + title: 'CPU Slice', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view_test.html new file mode 100644 index 00000000000..ee47fe5c58a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view_test.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/single_cpu_slice_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createBasicModel() { + const lines = [ + 'Android.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck + ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck + 'Android.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck + 'Android.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck + ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck + ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E', + ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck + ]; + + return tr.c.TestUtils.newModelWithEvents([lines.join('\n')], { + shiftWorldToZero: false + }); + } + + test('cpuSliceView_withCpuSliceOnExistingThread', function() { + const m = createBasicModel(); + + const cpu = m.kernel.cpus[1]; + assert.isDefined(cpu); + const cpuSlice = cpu.slices[0]; + assert.strictEqual('Binder_1', cpuSlice.title); + + const thread = m.findAllThreadsNamed('Binder_1')[0]; + assert.isDefined(thread); + assert.strictEqual(cpuSlice.threadThatWasRunning, thread); + + const view = document.createElement('tr-ui-a-single-cpu-slice-sub-view'); + const selection = new tr.model.EventSet(); + selection.push(cpuSlice); + view.selection = selection; + this.addHTMLOutput(view); + + // Clicking the analysis link should focus the Binder1's timeslice. + let didSelectionChangeHappen = false; + view.addEventListener('requestSelectionChange', function(e) { + assert.isTrue(e.selection.equals( + new tr.model.EventSet(thread.timeSlices[0]))); + didSelectionChangeHappen = true; + }); + Polymer.dom(view.root).querySelector('tr-ui-a-analysis-link').click(); + assert.isTrue(didSelectionChangeHappen); + }); + + test('cpuSliceViewWithCpuSliceOnMissingThread', function() { + const m = createBasicModel(); + + const cpu = m.kernel.cpus[1]; + assert.isDefined(cpu); + const cpuSlice = cpu.slices[1]; + assert.strictEqual('Android.launcher', cpuSlice.title); + assert.isUndefined(cpuSlice.thread); + + const selection = new tr.model.EventSet(); + selection.push(cpuSlice); + + const view = document.createElement('tr-ui-a-single-cpu-slice-sub-view'); + view.selection = selection; + this.addHTMLOutput(view); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view.html new file mode 100644 index 00000000000..d49125af9e3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view.html @@ -0,0 +1,356 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/analysis/stack_frame.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-single-event-sub-view'> + <template> + <style> + :host { + display: flex; + flex: 0 1; + flex-direction: column; + } + #table { + flex: 0 1 auto; + align-self: stretch; + font-size: 12px; + } + </style> + <tr-ui-b-table id="table"> + </tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-single-event-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + properties: { + isFlow: { + type: Boolean, + value: false + } + }, + + ready() { + this.currentSelection_ = undefined; + this.$.table.tableColumns = [ + { + title: 'Label', + value(row) { return row.name; }, + width: '150px' + }, + { + title: 'Value', + width: '100%', + value(row) { return row.value; } + } + ]; + this.$.table.showHeader = false; + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + if (selection.length !== 1) { + throw new Error('Only supports single slices'); + } + this.setSelectionWithoutErrorChecks(selection); + }, + + setSelectionWithoutErrorChecks(selection) { + this.currentSelection_ = selection; + this.updateContents_(); + }, + + getFlowEventRows_(event) { + // TODO(nduca): Figure out if there is a cleaner way to do this. + + const rows = this.getEventRowsHelper_(event); + + // Put the ID up top. + rows.splice(0, 0, { + name: 'ID', + value: event.id + }); + + function createLinkTo(slice) { + const linkEl = document.createElement('tr-ui-a-analysis-link'); + linkEl.setSelectionAndContent(function() { + return new tr.model.EventSet(slice); + }); + Polymer.dom(linkEl).textContent = slice.userFriendlyName; + return linkEl; + } + + rows.push({ + name: 'From', + value: createLinkTo(event.startSlice) + }); + rows.push({ + name: 'To', + value: createLinkTo(event.endSlice) + }); + return rows; + }, + + getEventRowsHelper_(event) { + const rows = []; + + if (event.error) { + rows.push({ name: 'Error', value: event.error }); + } + + if (event.title) { + let title = event.title; + if (tr.isExported('tr-ui-e-chrome-codesearch')) { + const container = document.createElement('div'); + container.appendChild(document.createTextNode(title)); + const link = document.createElement('tr-ui-e-chrome-codesearch'); + link.searchPhrase = title; + container.appendChild(link); + title = container; + } + rows.push({ name: 'Title', value: title }); + } + + if (event.category) { + rows.push({ name: 'Category', value: event.category }); + } + + if (event.model !== undefined) { + const ufc = event.model.getUserFriendlyCategoryFromEvent(event); + if (ufc !== undefined) { + rows.push({ name: 'User Friendly Category', value: ufc }); + } + } + + if (event.name) { + rows.push({ name: 'Name', value: event.name }); + } + + rows.push({ + name: 'Start', + value: tr.v.ui.createScalarSpan(event.start, { + unit: tr.b.Unit.byName.timeStampInMs + }) + }); + + if (event.duration) { + rows.push({ + name: 'Wall Duration', + value: tr.v.ui.createScalarSpan(event.duration, { + unit: tr.b.Unit.byName.timeDurationInMs + }) + }); + } + + if (event.cpuDuration) { + rows.push({ + name: 'CPU Duration', + value: tr.v.ui.createScalarSpan(event.cpuDuration, { + unit: tr.b.Unit.byName.timeDurationInMs + }) + }); + } + + if (event.subSlices !== undefined && event.subSlices.length !== 0) { + if (event.selfTime) { + rows.push({ + name: 'Self Time', + value: tr.v.ui.createScalarSpan(event.selfTime, { + unit: tr.b.Unit.byName.timeDurationInMs + }) + }); + } + + if (event.cpuSelfTime) { + const cpuSelfTimeEl = tr.v.ui.createScalarSpan(event.cpuSelfTime, { + unit: tr.b.Unit.byName.timeDurationInMs + }); + if (event.cpuSelfTime > event.selfTime) { + cpuSelfTimeEl.warning = + ' Note that CPU Self Time is larger than Self Time. ' + + 'This is a known limitation of this system, which occurs ' + + 'due to several subslices, rounding issues, and imprecise ' + + 'time at which we get cpu- and real-time.'; + } + rows.push({ name: 'CPU Self Time', value: cpuSelfTimeEl }); + } + } + + if (event.durationInUserTime) { + rows.push({ + name: 'Duration (U)', + value: tr.v.ui.createScalarSpan(event.durationInUserTime, { + unit: tr.b.Unit.byName.timeDurationInMs + }) + }); + } + + function createStackFrameEl(sf) { + const sfEl = document.createElement('tr-ui-a-stack-frame'); + sfEl.stackFrame = sf; + return sfEl; + } + if (event.startStackFrame && event.endStackFrame) { + if (event.startStackFrame === event.endStackFrame) { + rows.push({name: 'Start+End Stack Trace', + value: createStackFrameEl(event.startStackFrame)}); + } else { + rows.push({ name: 'Start Stack Trace', + value: createStackFrameEl(event.startStackFrame)}); + rows.push({ name: 'End Stack Trace', + value: createStackFrameEl(event.endStackFrame)}); + } + } else if (event.startStackFrame) { + rows.push({ name: 'Start Stack Trace', + value: createStackFrameEl(event.startStackFrame)}); + } else if (event.endStackFrame) { + rows.push({ name: 'End Stack Trace', + value: createStackFrameEl(event.endStackFrame)}); + } + + if (event.info) { + const descriptionEl = tr.ui.b.createDiv({ + textContent: event.info.description, + maxWidth: '300px' + }); + rows.push({ + name: 'Description', + value: descriptionEl + }); + + + if (event.info.docLinks) { + event.info.docLinks.forEach(function(linkObject) { + const linkEl = document.createElement('a'); + linkEl.target = '_blank'; + linkEl.href = linkObject.href; + Polymer.dom(linkEl).textContent = Polymer.dom(linkObject).textContent; + rows.push({ + name: linkObject.label, + value: linkEl + }); + }); + } + } + + if (event.associatedAlerts.length) { + const alertSubRows = []; + event.associatedAlerts.forEach(function(alert) { + const linkEl = document.createElement('tr-ui-a-analysis-link'); + linkEl.setSelectionAndContent(function() { + return new tr.model.EventSet(alert); + }, alert.info.description); + alertSubRows.push({ + name: alert.title, + value: linkEl + }); + }); + + rows.push({ + name: 'Alerts', value: '', + isExpanded: true, subRows: alertSubRows + }); + } + return rows; + }, + + getEventRows_(event) { + if (this.isFlow) { + return this.getFlowEventRows_(event); + } + + return this.getEventRowsHelper_(event); + }, + + addArgsToRows_(rows, args) { + let n = 0; + for (const argName in args) { + n += 1; + } + if (n > 0) { + const subRows = []; + for (const argName in args) { + n += 1; + } + if (n > 0) { + const subRows = []; + for (const argName in args) { + const argView = + document.createElement('tr-ui-a-generic-object-view'); + argView.object = args[argName]; + subRows.push({name: argName, value: argView}); + } + rows.push({ + name: 'Args', + value: '', + isExpanded: true, + subRows + }); + } + } + }, + + addContextsToRows_(rows, contexts) { + if (contexts.length) { + const subRows = contexts.map(function(context) { + const contextView = + document.createElement('tr-ui-a-generic-object-view'); + contextView.object = context; + return {name: 'Context', value: contextView}; + }); + rows.push({ + name: 'Contexts', + value: '', + isExpanded: true, + subRows + }); + } + }, + + updateContents_() { + if (this.currentSelection_ === undefined) { + this.$.table.rows = []; + this.$.table.rebuild(); + return; + } + + const event = tr.b.getOnlyElement(this.currentSelection_); + + const rows = this.getEventRows_(event); + if (event.argsStripped) { + rows.push({ name: 'Args', value: 'Stripped' }); + } else { + this.addArgsToRows_(rows, event.args); + } + this.addContextsToRows_(rows, event.contexts); + + const customizeRowsEvent = new tr.b.Event('customize-rows'); + customizeRowsEvent.rows = rows; + this.dispatchEvent(customizeRowsEvent); + + this.$.table.tableRows = rows; + this.$.table.rebuild(); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view_test.html new file mode 100644 index 00000000000..41c42308e3e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view_test.html @@ -0,0 +1,277 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const Thread = tr.model.Thread; + const EventSet = tr.model.EventSet; + const newSliceEx = tr.c.TestUtils.newSliceEx; + + function createSelection(customizeThreadCallback) { + const model = tr.c.TestUtils.newModelWithEvents([], { + customizeModelCallback(model) { + const t53 = model.getOrCreateProcess(52).getOrCreateThread(53); + customizeThreadCallback(t53, model); + } + }); + + const t53 = model.processes[52].threads[53]; + const t53track = {}; + t53track.thread = t53; + + const selection = new EventSet(); + selection.push(t53.sliceGroup.slices[0]); + assert.strictEqual(selection.length, 1); + + return selection; + } + + function createSelectionWithSingleSlice(opt_options) { + const options = opt_options || {}; + return createSelection(function(t53, model) { + let fA; + let fB; + if (options.withStartStackFrame || options.withEndStackFrame) { + fA = tr.c.TestUtils.newStackTrace(model, ['a1', 'a2']); + fB = tr.c.TestUtils.newStackTrace(model, ['b1', 'b2']); + } + + const slice = newSliceEx({title: 'b', start: 0, duration: 0.002}); + slice.category = options.withCategory ? 'foo' : ''; + + if (options.withStartStackFrame) { + slice.startStackFrame = options.withStartStackFrame === 'a' ? fA : fB; + } + + if (options.withEndStackFrame) { + slice.endStackFrame = options.withEndStackFrame === 'a' ? fA : fB; + } + + t53.sliceGroup.pushSlice(slice); + }); + } + + test('instantiate_withSingleSlice', function() { + const selection = createSelectionWithSingleSlice(); + + const analysisEl = document.createElement('tr-ui-a-single-event-sub-view'); + analysisEl.selection = selection; + this.addHTMLOutput(analysisEl); + }); + + test('alerts', function() { + const slice = newSliceEx({title: 'b', start: 0, duration: 0.002}); + + const ALERT_INFO_1 = new tr.model.EventInfo( + 'Alert 1', 'Critical alert'); + + const alert = new tr.model.Alert(ALERT_INFO_1, 5, [slice]); + + const selection = new EventSet(); + selection.push(slice); + + const analysisEl = document.createElement('tr-ui-a-single-event-sub-view'); + analysisEl.selection = selection; + this.addHTMLOutput(analysisEl); + }); + + test('instantiate_withSingleSliceWithArg', function() { + const selection = createSelection(function(t53) { + const slice = newSliceEx({title: 'my_slice', start: 0, duration: 1.0}); + slice.args = { + 'complex': { + 'b': '2 as a string', + 'c': [3, 4, 5] + } + }; + t53.sliceGroup.pushSlice(slice); + }); + + const subView = document.createElement('tr-ui-a-single-event-sub-view'); + subView.selection = selection; + this.addHTMLOutput(subView); + + const gov = tr.ui.b.findDeepElementMatching(subView, + 'tr-ui-a-generic-object-view'); + assert.isDefined(gov); + }); + + + test('instantiate_withSingleSliceCategory', function() { + const selection = createSelectionWithSingleSlice({withCategory: true}); + + const analysisEl = document.createElement('tr-ui-a-single-event-sub-view'); + analysisEl.selection = selection; + this.addHTMLOutput(analysisEl); + }); + + test('instantiate_withSingleStartStackFrame', function() { + const selection = createSelectionWithSingleSlice( + {withStartStackFrame: 'a'}); + + const analysisEl = document.createElement('tr-ui-a-single-event-sub-view'); + analysisEl.selection = selection; + this.addHTMLOutput(analysisEl); + + const e = tr.ui.b.findDeepElementWithTextContent( + analysisEl, /Start Stack Trace/); + assert.isDefined(e); + assert.isDefined(Polymer.dom(e).nextSibling.children[0].stackFrame); + }); + + test('instantiate_withSingleEndStackFrame', function() { + const selection = createSelectionWithSingleSlice( + {withEndStackFrame: 'b'}); + + const analysisEl = document.createElement('tr-ui-a-single-event-sub-view'); + analysisEl.selection = selection; + this.addHTMLOutput(analysisEl); + + const e = tr.ui.b.findDeepElementWithTextContent( + analysisEl, /End Stack Trace/); + assert.isDefined(e); + assert.isDefined(Polymer.dom(e).nextSibling.children[0].stackFrame); + assert.strictEqual( + Polymer.dom(e).nextSibling.children[0].stackFrame.title, 'b2'); + }); + + test('instantiate_withDifferentStartAndEndStackFrames', function() { + const selection = createSelectionWithSingleSlice( + {withStartStackFrame: 'a', + withEndStackFrame: 'b'}); + + const analysisEl = document.createElement('tr-ui-a-single-event-sub-view'); + analysisEl.selection = selection; + this.addHTMLOutput(analysisEl); + + const eA = tr.ui.b.findDeepElementWithTextContent( + analysisEl, /Start Stack Trace/); + assert.isDefined(eA); + assert.isDefined(Polymer.dom(eA).nextSibling.children[0].stackFrame); + assert.strictEqual( + Polymer.dom(eA).nextSibling.children[0].stackFrame.title, 'a2'); + + const eB = tr.ui.b.findDeepElementWithTextContent( + analysisEl, /End Stack Trace/); + assert.isDefined(eB); + assert.isDefined(Polymer.dom(eB).nextSibling.children[0].stackFrame); + assert.strictEqual( + Polymer.dom(eB).nextSibling.children[0].stackFrame.title, 'b2'); + }); + + test('instantiate_withSameStartAndEndStackFrames', function() { + const selection = createSelectionWithSingleSlice( + {withStartStackFrame: 'a', + withEndStackFrame: 'a'}); + + const analysisEl = document.createElement('tr-ui-a-single-event-sub-view'); + analysisEl.selection = selection; + this.addHTMLOutput(analysisEl); + + const e = tr.ui.b.findDeepElementWithTextContent( + analysisEl, /Start\+End Stack Trace/); + assert.isDefined(e); + assert.isDefined(Polymer.dom(e).nextSibling.children[0].stackFrame); + assert.strictEqual( + Polymer.dom(e).nextSibling.children[0].stackFrame.title, 'a2'); + }); + + test('analyzeSelectionWithSingleSlice', function() { + const selection = createSelectionWithSingleSlice(); + const subView = document.createElement('tr-ui-a-single-event-sub-view'); + subView.selection = selection; + this.addHTMLOutput(subView); + + const table = tr.ui.b.findDeepElementMatching( + subView, 'tr-ui-b-table'); + assert.strictEqual(table.tableRows.length, 3); + if (tr.isExported('tr-ui-e-chrome-codesearch')) { + assert.strictEqual(table.tableRows[0].value.innerText, 'b'); + } else { + assert.strictEqual(table.tableRows[0].value, 'b'); + } + assert.strictEqual(table.tableRows[1].value.value, 0); + assert.strictEqual(table.tableRows[1].value.unit, + tr.b.Unit.byName.timeStampInMs); + assert.strictEqual(table.tableRows[2].value.value, 0.002); + assert.strictEqual(table.tableRows[2].value.unit, + tr.b.Unit.byName.timeDurationInMs); + }); + + test('analyzeSelectionWithSingleSliceCategory', function() { + const selection = createSelectionWithSingleSlice({withCategory: true}); + + const subView = document.createElement('tr-ui-a-single-event-sub-view'); + subView.selection = selection; + this.addHTMLOutput(subView); + + const table = tr.ui.b.findDeepElementMatching( + subView, 'tr-ui-b-table'); + assert.strictEqual(table.tableRows.length, 4); + if (tr.isExported('tr-ui-e-chrome-codesearch')) { + assert.strictEqual(table.tableRows[0].value.innerText, 'b'); + } else { + assert.strictEqual(table.tableRows[0].value, 'b'); + } + assert.strictEqual(table.tableRows[1].value, 'foo'); + assert.strictEqual(table.tableRows[2].value.value, 0); + assert.strictEqual(table.tableRows[2].value.unit, + tr.b.Unit.byName.timeStampInMs); + assert.strictEqual(table.tableRows[3].value.value, 0.002); + assert.strictEqual(table.tableRows[3].value.unit, + tr.b.Unit.byName.timeDurationInMs); + }); + + test('instantiate_withSingleSliceContainingIDRef', function() { + const model = new Model(); + const p1 = model.getOrCreateProcess(1); + const myObjectSlice = p1.objects.addSnapshot( + '0x1000', 'cat', 'my_object', 0); + + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice(newSliceEx({title: 'b', start: 0, duration: 2})); + t1.sliceGroup.slices[0].args.my_object = myObjectSlice; + + const t1track = {}; + t1track.thread = t1; + + const selection = new EventSet(); + selection.push(t1.sliceGroup.slices[0]); + assert.strictEqual(selection.length, 1); + + const subView = document.createElement('tr-ui-a-single-event-sub-view'); + subView.selection = selection; + this.addHTMLOutput(subView); + + const analysisLink = tr.ui.b.findDeepElementMatching(subView, + 'tr-ui-a-analysis-link'); + assert.isDefined(analysisLink); + }); + + test('instantiate_withSingleSliceContainingInfo', function() { + const slice = newSliceEx({title: 'b', start: 0, duration: 1}); + slice.info = new tr.model.EventInfo( + 'Info title', 'Description'); + + const selection = new EventSet(); + selection.push(slice); + + const analysisEl = document.createElement('tr-ui-a-single-event-sub-view'); + analysisEl.selection = selection; + this.addHTMLOutput(analysisEl); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view.html new file mode 100644 index 00000000000..b201b161ffd --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html"> + +<dom-module id="tr-ui-a-single-flow-event-sub-view"> + <template> + <style> + :host { + display: block; + } + </style> + <tr-ui-a-single-event-sub-view id="singleEventSubView"> + </tr-ui-a-single-event-sub-view> + </template> +</dom-module> +<script> +'use strict'; + +function createAnalysisLinkTo(event) { + const linkEl = document.createElement('tr-ui-a-analysis-link'); + linkEl.setSelectionAndContent( + new tr.model.EventSet(event), event.userFriendlyName); + return linkEl; +} + +Polymer({ + is: 'tr-ui-a-single-flow-event-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + listeners: { + 'singleEventSubView.customize-rows': 'onCustomizeRows_' + }, + + set selection(selection) { + this.currentSelection_ = selection; + this.$.singleEventSubView.setSelectionWithoutErrorChecks(selection); + }, + + get selection() { + return this.currentSelection_; + }, + + /** + * Event handler for an event that's fired after the single event sub view has + * finished row construction. This hook gives us the opportunity to customize + * the rows present in the sub view. + */ + onCustomizeRows_(e) { + const event = tr.b.getOnlyElement(this.currentSelection_); + const rows = e.rows; + + rows.unshift({ + name: 'ID', + value: event.id + }); + rows.push({ + name: 'From', + value: createAnalysisLinkTo(event.startSlice) + }); + rows.push({ + name: 'To', + value: createAnalysisLinkTo(event.endSlice) + }); + } +}); +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-flow-event-sub-view', + tr.model.FlowEvent, + { + multi: false, + title: 'Flow Event', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view_test.html new file mode 100644 index 00000000000..31e3eb18f25 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view_test.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const EventSet = tr.model.EventSet; + const TestUtils = tr.c.TestUtils; + + test('analyzeSelectionWithSingleEvent', function() { + const model = TestUtils.newModel(function(model) { + model.p1 = model.getOrCreateProcess(1); + model.t2 = model.p1.getOrCreateThread(model.p1); + model.sA = model.t2.sliceGroup.pushSlice(TestUtils.newSliceEx({ + title: 'a', start: 0, end: 2 + })); + model.sB = model.t2.sliceGroup.pushSlice(TestUtils.newSliceEx({ + title: 'b', start: 9, end: 11 + })); + model.fe = TestUtils.newFlowEventEx({ + cat: 'cat', + id: 1234, + title: 'MyFlow', + start: 1, + end: 10, + startSlice: model.sA, + endSlice: model.sB + }); + model.flowEvents.push(model.fe); + }); + + const selection = new EventSet(); + selection.push(model.fe); + assert.strictEqual(selection.length, 1); + + const subView = document.createElement('tr-ui-a-single-event-sub-view'); + subView.isFlow = true; + subView.selection = selection; + this.addHTMLOutput(subView); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_frame_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_frame_sub_view.html new file mode 100644 index 00000000000..e89fa2626ef --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_frame_sub_view.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/analysis/alert_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> + +<dom-module id='tr-ui-a-single-frame-sub-view'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + #asv { + flex: 0 0 auto; + align-self: stretch; + } + </style> + <tr-ui-a-alert-sub-view id="asv"> + </tr-ui-a-alert-sub-view> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-single-frame-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + ready() { + this.currentSelection_ = undefined; + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + this.currentSelection_ = selection; + this.$.asv.selection = tr.b.getOnlyElement(selection).associatedAlerts; + }, + + get relatedEventsToHighlight() { + if (!this.currentSelection_) return undefined; + return tr.b.getOnlyElement(this.currentSelection_).associatedEvents; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-frame-sub-view', + tr.model.Frame, + { + multi: false, + title: 'Frame', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view.html new file mode 100644 index 00000000000..43b0e8a80cd --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html"> + +<dom-module id='tr-ui-a-single-instant-event-sub-view'> + <template> + <style> + :host { + display: block; + } + </style> + <div id='content'></div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-single-instant-event-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.currentSelection_ = undefined; + }, + + set selection(selection) { + Polymer.dom(this.$.content).textContent = ''; + const realView = document.createElement('tr-ui-a-single-event-sub-view'); + realView.setSelectionWithoutErrorChecks(selection); + + Polymer.dom(this.$.content).appendChild(realView); + + this.currentSelection_ = selection; + }, + + get selection() { + return this.currentSelection_; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-instant-event-sub-view', + tr.model.InstantEvent, + { + multi: false, + title: 'Instant Event', + }); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-multi-instant-event-sub-view', + tr.model.InstantEvent, + { + multi: true, + title: 'Instant Events', + }); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view_test.html new file mode 100644 index 00000000000..4ad85d2e6db --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view_test.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const Thread = tr.model.Thread; + const EventSet = tr.model.EventSet; + + test('analyzeSelectionWithSingleEvent', function() { + const model = new Model(); + const p52 = model.getOrCreateProcess(52); + const t53 = p52.getOrCreateThread(53); + + const ie = new tr.model.ProcessInstantEvent('cat', 'title', 7, 10, {}); + ie.duration = 20; + p52.instantEvents.push(ie); + + + const selection = new EventSet(); + selection.push(ie); + assert.strictEqual(selection.length, 1); + + const subView = document.createElement( + 'tr-ui-a-single-instant-event-sub-view'); + subView.selection = selection; + + this.addHTMLOutput(subView); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view.html new file mode 100644 index 00000000000..49810ab3fbd --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view.html @@ -0,0 +1,129 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/analysis/object_instance_view.html"> +<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html"> + +<dom-module id='tr-ui-a-single-object-instance-sub-view'> + <template> + <style> + :host { + display: block; + } + + #snapshots > * { + display: block; + } + + :host { + overflow: auto; + display: block; + } + + * { + -webkit-user-select: text; + } + + .title { + border-bottom: 1px solid rgb(128, 128, 128); + font-size: 110%; + font-weight: bold; + } + + td, th { + font-family: monospace; + vertical-align: top; + } + </style> + <div id='content'></div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-single-object-instance-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.currentSelection_ = undefined; + }, + + get requiresTallView() { + if (this.$.content.children.length === 0) { + return false; + } + if (this.$.content.children[0] instanceof + tr.ui.analysis.ObjectInstanceView) { + return this.$.content.children[0].requiresTallView; + } + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + const instance = tr.b.getOnlyElement(selection); + if (!(instance instanceof tr.model.ObjectInstance)) { + throw new Error('Only supports object instances'); + } + + Polymer.dom(this.$.content).textContent = ''; + this.currentSelection_ = selection; + + const typeInfo = tr.ui.analysis.ObjectInstanceView.getTypeInfo( + instance.category, instance.typeName); + if (typeInfo) { + const customView = new typeInfo.constructor(); + Polymer.dom(this.$.content).appendChild(customView); + customView.modelEvent = instance; + } else { + this.appendGenericAnalysis_(instance); + } + }, + + appendGenericAnalysis_(instance) { + let html = ''; + html += '<div class="title">' + + instance.typeName + ' ' + + instance.id + '</div>\n'; + html += '<table>'; + html += '<tr>'; + html += '<tr><td>creationTs:</td><td>' + + instance.creationTs + '</td></tr>\n'; + if (instance.deletionTs !== Number.MAX_VALUE) { + html += '<tr><td>deletionTs:</td><td>' + + instance.deletionTs + '</td></tr>\n'; + } else { + html += '<tr><td>deletionTs:</td><td>not deleted</td></tr>\n'; + } + html += '<tr><td>snapshots:</td><td id="snapshots"></td></tr>\n'; + html += '</table>'; + Polymer.dom(this.$.content).innerHTML = html; + const snapshotsEl = Polymer.dom(this.$.content).querySelector('#snapshots'); + instance.snapshots.forEach(function(snapshot) { + const snapshotLink = document.createElement('tr-ui-a-analysis-link'); + snapshotLink.selection = new tr.model.EventSet(snapshot); + Polymer.dom(snapshotsEl).appendChild(snapshotLink); + }); + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-object-instance-sub-view', + tr.model.ObjectInstance, + { + multi: false, + title: 'Object Instance', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view_test.html new file mode 100644 index 00000000000..f5414dd957a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view_test.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/single_object_instance_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ObjectInstance = tr.model.ObjectInstance; + + test('analyzeSelectionWithObjectInstanceUnknownType', function() { + const i10 = new ObjectInstance( + {}, '0x1000', 'cat', 'someUnhandledName', 10); + const s10 = i10.addSnapshot(10, {foo: 1}); + const s20 = i10.addSnapshot(20, {foo: 2}); + + const selection = new tr.model.EventSet(); + selection.push(i10); + + const view = + document.createElement('tr-ui-a-single-object-instance-sub-view'); + view.selection = selection; + this.addHTMLOutput(view); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view.html new file mode 100644 index 00000000000..5565db8d004 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view.html @@ -0,0 +1,142 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/analysis/object_instance_view.html"> +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> +<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-single-object-snapshot-sub-view'> + <template> + <style> + #args { + white-space: pre; + } + + :host { + overflow: auto; + display: flex; + } + + ::content * { + -webkit-user-select: text; + } + + ::content .title { + border-bottom: 1px solid rgb(128, 128, 128); + font-size: 110%; + font-weight: bold; + } + + ::content td, th { + font-family: monospace; + vertical-align: top; + } + </style> + <slot></slot> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-single-object-snapshot-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.currentSelection_ = undefined; + }, + + get requiresTallView() { + if (this.children.length === 0) { + return false; + } + if (this.children[0] instanceof tr.ui.analysis.ObjectSnapshotView) { + return this.children[0].requiresTallView; + } + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + const snapshot = tr.b.getOnlyElement(selection); + if (!(snapshot instanceof tr.model.ObjectSnapshot)) { + throw new Error('Only supports object instances'); + } + + Polymer.dom(this).textContent = ''; + this.currentSelection_ = selection; + + const typeInfo = tr.ui.analysis.ObjectSnapshotView.getTypeInfo( + snapshot.objectInstance.category, snapshot.objectInstance.typeName); + if (typeInfo) { + const customView = new typeInfo.constructor(); + Polymer.dom(this).appendChild(customView); + customView.modelEvent = snapshot; + } else { + this.appendGenericAnalysis_(snapshot); + } + }, + + appendGenericAnalysis_(snapshot) { + const instance = snapshot.objectInstance; + + Polymer.dom(this).textContent = ''; + + const titleEl = document.createElement('div'); + Polymer.dom(titleEl).classList.add('title'); + Polymer.dom(titleEl).appendChild(document.createTextNode('Snapshot of ')); + Polymer.dom(this).appendChild(titleEl); + + const instanceLinkEl = document.createElement('tr-ui-a-analysis-link'); + instanceLinkEl.selection = new tr.model.EventSet(instance); + Polymer.dom(titleEl).appendChild(instanceLinkEl); + + Polymer.dom(titleEl).appendChild(document.createTextNode(' @ ')); + + Polymer.dom(titleEl).appendChild(tr.v.ui.createScalarSpan(snapshot.ts, { + unit: tr.b.Unit.byName.timeStampInMs, + ownerDocument: this.ownerDocument, + inline: true, + })); + + const tableEl = document.createElement('table'); + Polymer.dom(this).appendChild(tableEl); + + const rowEl = document.createElement('tr'); + Polymer.dom(tableEl).appendChild(rowEl); + + const labelEl = document.createElement('td'); + Polymer.dom(labelEl).textContent = 'args:'; + Polymer.dom(rowEl).appendChild(labelEl); + + const argsEl = document.createElement('td'); + argsEl.id = 'args'; + Polymer.dom(rowEl).appendChild(argsEl); + + const objectViewEl = document.createElement('tr-ui-a-generic-object-view'); + objectViewEl.object = snapshot.args; + Polymer.dom(argsEl).appendChild(objectViewEl); + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-object-snapshot-sub-view', + tr.model.ObjectSnapshot, + { + multi: false, + title: 'Object Snapshot', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view_test.html new file mode 100644 index 00000000000..41fca173931 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view_test.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/single_object_snapshot_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate_snapshotView', function() { + const i10 = new tr.model.ObjectInstance( + {}, '0x1000', 'cat', 'name', 10); + const s10 = i10.addSnapshot(10, {foo: 1}); + i10.updateBounds(); + + const selection = new tr.model.EventSet(); + selection.push(s10); + + const view = + document.createElement('tr-ui-a-single-object-snapshot-sub-view'); + view.selection = selection; + this.addHTMLOutput(view); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view.html new file mode 100644 index 00000000000..7396cfa3eca --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-power-sample-table'> + <template> + <style> + :host { + display: flex; + font-size: 12px; + } + </style> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> + +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-power-sample-table', + + ready() { + this.$.table.tableColumns = [ + { + title: 'Time', + width: '100px', + value(row) { + return tr.v.ui.createScalarSpan(row.start, { + unit: tr.b.Unit.byName.timeStampInMs + }); + } + }, + { + title: 'Power', + width: '100%', + value(row) { + return tr.v.ui.createScalarSpan(row.powerInW, { + unit: tr.b.Unit.byName.powerInWatts + }); + } + } + ]; + this.sample = undefined; + }, + + get sample() { + return this.sample_; + }, + + set sample(sample) { + this.sample_ = sample; + this.updateContents_(); + }, + + updateContents_() { + if (this.sample === undefined) { + this.$.table.tableRows = []; + } else { + this.$.table.tableRows = [this.sample]; + } + this.$.table.rebuild(); + } +}); +</script> + +<dom-module id='tr-ui-a-single-power-sample-sub-view'> + <template> + <style> + :host { display: block; } + </style> + <tr-ui-a-power-sample-table id="samplesTable"> + </tr-ui-a-power-sample-table> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-single-power-sample-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + ready() { + this.currentSelection_ = undefined; + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + this.currentSelection_ = selection; + this.updateContents_(); + }, + + updateContents_() { + if (this.selection.length !== 1) { + throw new Error('Cannot pass multiple samples to sample table.'); + } + this.$.samplesTable.sample = tr.b.getOnlyElement(this.selection); + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-power-sample-sub-view', + tr.model.PowerSample, + { + multi: false, + title: 'Power Sample', + }); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view_test.html new file mode 100644 index 00000000000..8ee1dfcf899 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view_test.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/power_series.html"> +<link rel="import" href="/tracing/ui/analysis/single_power_sample_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const model = new tr.Model(); + const series = new tr.model.PowerSeries(model.device); + series.addPowerSample(1, 1); + + const view = document.createElement('tr-ui-a-single-power-sample-sub-view'); + view.selection = new tr.model.EventSet(series.samples); + + this.addHTMLOutput(view); + }); + + test('setSelection', function() { + const model = new tr.Model(); + const series = new tr.model.PowerSeries(model.device); + series.addPowerSample(1, 1); + + const view = document.createElement('tr-ui-a-single-power-sample-sub-view'); + const eventSet = new tr.model.EventSet(series.samples); + view.selection = eventSet; + + assert.deepEqual(view.$.samplesTable.sample, + tr.b.getOnlyElement(series.samples)); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view.html new file mode 100644 index 00000000000..851c60952ff --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/stack_frame.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-single-sample-sub-view'> + <template> + <style> + :host { + display: flex; + font-size: 12px; + } + </style> + <tr-ui-b-table id="content"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-single-sample-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.currentSelection_ = undefined; + }, + + ready() { + this.$.content.tableColumns = [ + { + title: '', + value: row => row.title, + width: '100px' + }, + { + title: '', + value: row => row.value, + width: '100%' + } + ]; + this.$.content.showHeader = false; + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + this.currentSelection_ = selection; + + if (this.currentSelection_ === undefined) { + this.$.content.tableRows = []; + return; + } + + const sample = tr.b.getOnlyElement(this.currentSelection_); + const table = this.$.content; + const rows = []; + + rows.push({ + title: 'Title', + value: sample.title + }); + + rows.push({ + title: 'Sample time', + value: tr.v.ui.createScalarSpan(sample.start, { + unit: tr.b.Unit.byName.timeStampInMs, + ownerDocument: this.ownerDocument + }) + }); + + const callStackTableEl = document.createElement('tr-ui-b-table'); + callStackTableEl.tableRows = sample.getNodesAsArray().reverse(); + callStackTableEl.tableColumns = [ + { + title: 'function name', + value: row => row.functionName || '(anonymous function)' + }, + { + title: 'location', + value: row => row.url + } + ]; + callStackTableEl.rebuild(); + rows.push({ + title: 'Call stack', + value: callStackTableEl + }); + table.tableRows = rows; + table.rebuild(); + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-sample-sub-view', + tr.model.Sample, + { + multi: false, + title: 'Sample', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view_test.html new file mode 100644 index 00000000000..7f8c131f82c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view_test.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/single_sample_sub_view.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const EventSet = tr.model.EventSet; + const newSampleNamed = tr.c.TestUtils.newSampleNamed; + + test('instantiate_withSingleSample', function() { + let t53; + const model = tr.c.TestUtils.newModelWithEvents([], { + shiftWorldToZero: false, + pruneContainers: false, + customizeModelCallback(model) { + t53 = model.getOrCreateProcess(52).getOrCreateThread(53); + model.samples.push(newSampleNamed(t53, 'X', 'my-category', + ['a', 'b', 'c'], 0.184)); + } + }); + + const t53track = {}; + t53track.thread = t53; + + const selection = new EventSet(); + + assert.strictEqual(selection.length, 0); + selection.push(t53.samples[0]); + assert.strictEqual(selection.length, 1); + + const view = document.createElement('tr-ui-a-single-sample-sub-view'); + view.selection = selection; + this.addHTMLOutput(view); + + const table = tr.ui.b.findDeepElementMatching( + view, 'tr-ui-b-table'); + + const rows = table.tableRows; + assert.strictEqual(rows.length, 3); + assert.strictEqual(rows[0].value, 'X'); + assert.strictEqual(rows[1].value.value, 0.184); + assert.strictEqual(rows[1].value.unit, tr.b.Unit.byName.timeStampInMs); + + const callStackRows = rows[2].value.tableRows; + assert.lengthOf(callStackRows, 3); + assert.deepEqual(callStackRows.map(x => x.title), ['a', 'b', 'c']); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view.html new file mode 100644 index 00000000000..720fdfeb65a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/related_events.html"> +<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html"> + +<dom-module id='tr-ui-a-single-thread-slice-sub-view'> + <template> + <style> + :host { + display: flex; + flex-direction: row; + } + #events { + display: flex; + flex-direction: column; + } + + </style> + <tr-ui-a-single-event-sub-view id="content"></tr-ui-a-single-event-sub-view> + <div id="events"> + <tr-ui-a-related-events id="relatedEvents"> + </tr-ui-a-related-events> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-single-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.content.selection = selection; + this.$.relatedEvents.setRelatedEvents(selection); + if (this.$.relatedEvents.hasRelatedEvents()) { + this.$.relatedEvents.style.display = ''; + } else { + this.$.relatedEvents.style.display = 'none'; + } + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-thread-slice-sub-view', + tr.model.ThreadSlice, + { + multi: false, + title: 'Slice', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view_test.html new file mode 100644 index 00000000000..84bb292384e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view_test.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/thread_slice.html"> +<link rel="import" href="/tracing/ui/analysis/single_thread_slice_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + const newFlowEventEx = tr.c.TestUtils.newFlowEventEx; + + test('instantiate', function() { + const model = new tr.Model(); + const t53 = model.getOrCreateProcess(52).getOrCreateThread(53); + t53.sliceGroup.pushSlice( + newSliceEx({title: 'a', start: 0.0, duration: 0.5})); + t53.sliceGroup.createSubSlices(); + + const selection = new tr.model.EventSet(); + selection.push(t53.sliceGroup.slices[0]); + + const viewEl = document.createElement( + 'tr-ui-a-single-thread-slice-sub-view'); + viewEl.selection = selection; + this.addHTMLOutput(viewEl); + }); + + test('instantiateWithFlowEvent', function() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + + m.t2 = m.p1.getOrCreateThread(2); + m.t3 = m.p1.getOrCreateThread(3); + m.t4 = m.p1.getOrCreateThread(4); + + m.sA = m.t2.sliceGroup.pushSlice( + newSliceEx({title: 'a', start: 0, end: 5, + type: tr.model.ThreadSlice})); + m.sB = m.t3.sliceGroup.pushSlice( + newSliceEx({title: 'b', start: 10, end: 15, + type: tr.model.ThreadSlice})); + m.sC = m.t4.sliceGroup.pushSlice( + newSliceEx({title: 'c', start: 20, end: 20, + type: tr.model.ThreadSlice})); + + m.t2.createSubSlices(); + m.t3.createSubSlices(); + m.t4.createSubSlices(); + + m.f1 = newFlowEventEx({ + title: 'flowish', start: 0, end: 10, + startSlice: m.sA, + endSlice: m.sB + }); + m.f2 = newFlowEventEx({ + title: 'flowish', start: 15, end: 21, + startSlice: m.sB, + endSlice: m.sC + }); + }); + + const selection = new tr.model.EventSet(); + selection.push(m.sA); + + const viewEl = document.createElement( + 'tr-ui-a-single-thread-slice-sub-view'); + viewEl.selection = selection; + this.addHTMLOutput(viewEl); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view.html new file mode 100644 index 00000000000..225b2729769 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view.html @@ -0,0 +1,186 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-single-thread-time-slice-sub-view'> + <template> + <style> + table { + border-collapse: collapse; + border-width: 0; + margin-bottom: 25px; + width: 100%; + } + + table tr > td:first-child { + padding-left: 2px; + } + + table tr > td { + padding: 2px 4px 2px 4px; + vertical-align: text-top; + width: 150px; + } + + table td td { + padding: 0 0 0 0; + width: auto; + } + tr { + vertical-align: top; + } + + tr:nth-child(2n+0) { + background-color: #e2e2e2; + } + </style> + <table> + <tr> + <td>Running process:</td><td id="process-name"></td> + </tr> + <tr> + <td>Running thread:</td><td id="thread-name"></td> + </tr> + <tr> + <td>State:</td> + <td><b><span id="state"></span></b></td> + </tr> + <tr> + <td>Start:</td> + <td> + <tr-v-ui-scalar-span id="start"> + </tr-v-ui-scalar-span> + </td> + </tr> + <tr> + <td>Duration:</td> + <td> + <tr-v-ui-scalar-span id="duration"> + </tr-v-ui-scalar-span> + </td> + </tr> + + <tr> + <td>On CPU:</td><td id="on-cpu"></td> + </tr> + + <tr> + <td>Running instead:</td><td id="running-instead"></td> + </tr> + + <tr> + <td>Args:</td><td id="args"></td> + </tr> + </table> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-single-thread-time-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.currentSelection_ = undefined; + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + const timeSlice = tr.b.getOnlyElement(selection); + + if (!(timeSlice instanceof tr.model.ThreadTimeSlice)) { + throw new Error('Only supports thread time slices'); + } + + this.currentSelection_ = selection; + + const thread = timeSlice.thread; + + const root = Polymer.dom(this.root); + Polymer.dom(root.querySelector('#state')).textContent = + timeSlice.title; + const stateColor = tr.b.ColorScheme.colorsAsStrings[timeSlice.colorId]; + root.querySelector('#state').style.backgroundColor = stateColor; + + Polymer.dom(root.querySelector('#process-name')).textContent = + thread.parent.userFriendlyName; + Polymer.dom(root.querySelector('#thread-name')).textContent = + thread.userFriendlyName; + + root.querySelector('#start').setValueAndUnit( + timeSlice.start, tr.b.Unit.byName.timeStampInMs); + root.querySelector('#duration').setValueAndUnit( + timeSlice.duration, tr.b.Unit.byName.timeDurationInMs); + + const onCpuEl = root.querySelector('#on-cpu'); + Polymer.dom(onCpuEl).textContent = ''; + const runningInsteadEl = root.querySelector('#running-instead'); + if (timeSlice.cpuOnWhichThreadWasRunning) { + Polymer.dom(runningInsteadEl.parentElement).removeChild(runningInsteadEl); + + const cpuLink = document.createElement('tr-ui-a-analysis-link'); + cpuLink.selection = new tr.model.EventSet( + timeSlice.getAssociatedCpuSlice()); + Polymer.dom(cpuLink).textContent = + timeSlice.cpuOnWhichThreadWasRunning.userFriendlyName; + Polymer.dom(onCpuEl).appendChild(cpuLink); + } else { + Polymer.dom(onCpuEl.parentElement).removeChild(onCpuEl); + + const cpuSliceThatTookCpu = timeSlice.getCpuSliceThatTookCpu(); + if (cpuSliceThatTookCpu) { + const cpuLink = document.createElement('tr-ui-a-analysis-link'); + cpuLink.selection = new tr.model.EventSet(cpuSliceThatTookCpu); + if (cpuSliceThatTookCpu.thread) { + Polymer.dom(cpuLink).textContent = + cpuSliceThatTookCpu.thread.userFriendlyName; + } else { + Polymer.dom(cpuLink).textContent = cpuSliceThatTookCpu.title; + } + Polymer.dom(runningInsteadEl).appendChild(cpuLink); + } else { + Polymer.dom(runningInsteadEl.parentElement).removeChild( + runningInsteadEl); + } + } + + const argsEl = root.querySelector('#args'); + if (Object.keys(timeSlice.args).length > 0) { + const argsView = + document.createElement('tr-ui-a-generic-object-view'); + argsView.object = timeSlice.args; + + argsEl.parentElement.style.display = ''; + Polymer.dom(argsEl).textContent = ''; + Polymer.dom(argsEl).appendChild(argsView); + } else { + argsEl.parentElement.style.display = 'none'; + } + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-thread-time-slice-sub-view', + tr.model.ThreadTimeSlice, + { + multi: false, + title: 'Thread Timeslice', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view_test.html new file mode 100644 index 00000000000..bfffd41861b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view_test.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/single_thread_time_slice_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createBasicModel() { + const lines = [ + 'Android.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck + ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck + 'Android.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck + 'Android.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck + ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck + ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E', + ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck + ]; + + return tr.c.TestUtils.newModelWithEvents([lines.join('\n')], { + shiftWorldToZero: false + }); + } + + test('runningSlice', function() { + const m = createBasicModel(); + + const cpu = m.kernel.cpus[1]; + const binderSlice = cpu.slices[0]; + assert.strictEqual(binderSlice.title, 'Binder_1'); + const launcherSlice = cpu.slices[1]; + assert.strictEqual(launcherSlice.title, 'Android.launcher'); + + + const thread = m.findAllThreadsNamed('Binder_1')[0]; + + const view = document.createElement( + 'tr-ui-a-single-thread-time-slice-sub-view'); + const selection = new tr.model.EventSet(); + selection.push(thread.timeSlices[0]); + view.selection = selection; + this.addHTMLOutput(view); + + // Clicking the analysis link should focus the Binder1's timeslice. + let didSelectionChangeHappen = false; + view.addEventListener('requestSelectionChange', function(e) { + assert.isTrue(e.selection.equals(new tr.model.EventSet(binderSlice))); + didSelectionChangeHappen = true; + }); + Polymer.dom(view.root).querySelector('tr-ui-a-analysis-link').click(); + assert.isTrue(didSelectionChangeHappen); + }); + + test('sleepingSlice', function() { + const m = createBasicModel(); + + const cpu = m.kernel.cpus[1]; + const binderSlice = cpu.slices[0]; + assert.strictEqual(binderSlice.title, 'Binder_1'); + const launcherSlice = cpu.slices[1]; + assert.strictEqual(launcherSlice.title, 'Android.launcher'); + + + const thread = m.findAllThreadsNamed('Binder_1')[0]; + + const view = document.createElement( + 'tr-ui-a-single-thread-time-slice-sub-view'); + const selection = new tr.model.EventSet(); + selection.push(thread.timeSlices[1]); + view.selection = selection; + this.addHTMLOutput(view); + + // Clicking the analysis link should focus the Android.launcher slice + let didSelectionChangeHappen = false; + view.addEventListener('requestSelectionChange', function(e) { + assert.isTrue(e.selection.equals(new tr.model.EventSet(launcherSlice))); + didSelectionChangeHappen = true; + }); + Polymer.dom(view.root).querySelector('tr-ui-a-analysis-link').click(); + assert.isTrue(didSelectionChangeHappen); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_user_expectation_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_user_expectation_sub_view.html new file mode 100644 index 00000000000..76110b4b468 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_user_expectation_sub_view.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html"> +<link rel="import" + href="/tracing/ui/analysis/user_expectation_related_samples_table.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-a-single-user-expectation-sub-view'> + <template> + <style> + :host { + display: flex; + flex-direction: row; + } + #events { + display: flex; + flex-direction: column; + } + </style> + <tr-ui-a-single-event-sub-view id="realView"></tr-ui-a-single-event-sub-view> + <div id="events"> + <tr-ui-a-user-expectation-related-samples-table id="relatedSamples"></tr-ui-a-user-expectation-related-samples-table> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-single-user-expectation-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + created() { + this.currentSelection_ = undefined; + }, + + get selection() { + return this.currentSelection_; + }, + + set selection(selection) { + this.$.realView.addEventListener('customize-rows', + this.onCustomizeRows_.bind(this)); + + this.currentSelection_ = selection; + this.$.realView.setSelectionWithoutErrorChecks(selection); + + this.$.relatedSamples.selection = selection; + if (this.$.relatedSamples.hasRelatedSamples()) { + this.$.events.style.display = ''; + } else { + this.$.events.style.display = 'none'; + } + }, + + get relatedEventsToHighlight() { + if (!this.currentSelection_) return undefined; + return tr.b.getOnlyElement(this.currentSelection_).associatedEvents; + }, + + onCustomizeRows_(event) { + const ue = tr.b.getOnlyElement(this.selection); + + if (ue.rawCpuMs) { + event.rows.push({ + name: 'Total CPU', + value: tr.v.ui.createScalarSpan(ue.totalCpuMs, { + unit: tr.b.Unit.byName.timeDurationInMs + }) + }); + } + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-single-user-expectation-sub-view', + tr.model.um.UserExpectation, + { + multi: false, + title: 'User Expectation', + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame.html new file mode 100644 index 00000000000..92c4594af5f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-a-stack-frame'> + <template> + <style> + :host { + display: flex; + flex-direction: row; + align-items: center; + font-size: 12px; + } + </style> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-stack-frame', + + ready() { + this.stackFrame_ = undefined; + this.$.table.tableColumns = []; + this.$.table.showHeader = true; + }, + + get stackFrame() { + return this.stackFrame_; + }, + + set stackFrame(stackFrame) { + const table = this.$.table; + + this.stackFrame_ = stackFrame; + if (stackFrame === undefined) { + table.tableColumns = []; + table.tableRows = []; + table.rebuild(); + return; + } + + let hasName = false; + let hasTitle = false; + + table.tableRows = stackFrame.stackTrace; + table.tableRows.forEach(function(row) { + hasName |= row.name !== undefined; + hasTitle |= row.title !== undefined; + }); + + const cols = []; + if (hasName) { + cols.push({ + title: 'Name', + value(row) { return row.name; } + }); + } + + if (hasTitle) { + cols.push({ + title: 'Title', + value(row) { return row.title; } + }); + } + + table.tableColumns = cols; + table.rebuild(); + }, + + tableForTesting() { + return this.$.table; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame_test.html new file mode 100644 index 00000000000..4523906a321 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame_test.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/analysis/stack_frame.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const model = new tr.Model(); + const fA = tr.c.TestUtils.newStackTrace(model, ['a1', 'a2', 'a3']); + + const stackFrameView = document.createElement('tr-ui-a-stack-frame'); + stackFrameView.stackFrame = fA; + this.addHTMLOutput(stackFrameView); + }); + + test('clearingStackFrame', function() { + const model = new tr.Model(); + const fA = tr.c.TestUtils.newStackTrace(model, ['a1', 'a2', 'a3']); + + const stackFrameView = document.createElement('tr-ui-a-stack-frame'); + stackFrameView.stackFrame = fA; + stackFrameView.stackFrame = undefined; + + assert.isUndefined(stackFrameView.stackFrame); + assert.lengthOf(stackFrameView.$.table.$.body.children, 0); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane.html new file mode 100644 index 00000000000..0e4f633fb00 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/rebuildable_behavior.html"> + +<!-- +@fileoverview Analysis view stacked pane. See the stacked pane view element +(tr-ui-a-stacked-pane-view) documentation for more details. +--> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + const StackedPaneImpl = { + /** + * Request changing the child pane of this pane in the associated stacked + * pane view. If the assigned builder is undefined, request removing the + * current child pane. + * + * Note that setting this property before appended() is called will have no + * effect (as there will be no listener attached to the pane). + * + * This method is intended to be called by subclasses. + */ + set childPaneBuilder(childPaneBuilder) { + this.childPaneBuilder_ = childPaneBuilder; + this.dispatchEvent(new tr.b.Event('request-child-pane-change')); + }, + + get childPaneBuilder() { + return this.childPaneBuilder_; + }, + + /** + * Called right after the pane is appended to a pane view. + * + * This method triggers an immediate rebuild by default. Subclasses are + * free to change this behavior (e.g. if a pane has lots of data to display, + * it might decide to defer rebuilding in order not to cause jank). + */ + appended() { + this.rebuild(); + } + }; + + const StackedPane = [tr.ui.analysis.RebuildableBehavior, StackedPaneImpl]; + + return { + StackedPane, + }; +}); + +Polymer({ + is: 'tr-ui-a-stacked-pane', + behaviors: [tr.ui.analysis.StackedPane] +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_test.html new file mode 100644 index 00000000000..7af70fa3d7a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_test.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/stacked_pane.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('changeChildPane', function() { + const pane = document.createElement('tr-ui-a-stacked-pane'); + let didFireEvent; + pane.addEventListener('request-child-pane-change', function() { + didFireEvent = true; + }); + + didFireEvent = false; + pane.childPaneBuilder = undefined; + assert.isTrue(didFireEvent); + + didFireEvent = false; + pane.childPaneBuilder = function() { + return undefined; + }; + assert.isTrue(didFireEvent); + + didFireEvent = false; + pane.childPaneBuilder = function() { + return document.createElement('tr-ui-a-stacked-pane'); + }; + assert.isTrue(didFireEvent); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view.html new file mode 100644 index 00000000000..e8bea2dd034 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view.html @@ -0,0 +1,195 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +<!-- +@fileoverview Analysis view container which displays vertically stacked panes. +The panes represent a hierarchy where a child pane contains the details of the +current selection in its parent pane. The container provides simple primitives +for panes to request changing their child pane: + + +=<tr-ui-a-stacked-pane-view>=+ +=<tr-ui-a-stacked-pane-view>=+ + |+.<tr-ui-a-stacked-pane>....+| |+.<tr-ui-a-stacked-pane>....+| + |: Pane 1 +| ===========> |: Pane 1 +| + |+...........................+| Pane 1 |+...........................+| + |+.<tr-ui-a-stacked-pane>....+| requests |+.<tr-ui-a-stacked-pane>....+| + |: Pane 2 (detail of Pane 1) +| child pane |: Pane 4 (detail of Pane 1) +| + |+...........................+| change (e.g. |+...........................+| + |+.<tr-ui-a-stacked-pane>....+| selection +=============================+ + |: Pane 3 (detail of Pane 2) +| changed) + |+...........................+| + +=============================+ + +Note that the actual UI provided by tr-ui-a-stacked-pane-view and +tr-ui-a-stacked-pane is merely a wrapper container with flex box vertical +stacking. No other visual features (such as pane spacing or borders) is +provided by either element. + +The stacked pane element (tr-ui-a-stacked-pane) is defined in a separate file. + +Sample use case: + + Create an empty stacked pane view and add it to the DOM: + + const paneView = document.createElement('tr-ui-a-stacked-pane-view'); + Polymer.dom(someParentView).appendChild(paneView); + + Define one or more pane subclasses: + + TODO(polymer): Write this documentation + <polymer-element name="some-pane-1" extends="tr-ui-a-stacked-pane"> + ... + </polymer-element> + + Set the top-level pane (by providing a builder function): + + paneView.setPaneBuilder(function() { + const topPane = document.createElement('some-pane-1'); + pane.someProperty = someValue; + return topPane; + }); + + Show a child pane with details upon user interaction (these methods should be + in the definition of the pane subclass Polymer element): + + ready: function() { + this.$.table.addEventListener( + 'selection-changed', this.changeChildPane_.bind(this)); + } + + changeChildPane_: function() { + this.childPaneBuilder = function() { + const selectedRow = this.$.table.selectedTableRow; + const detailsPane = document.createElement('some-pane-2'); + detailsPane.someProperty = selectedRow; + return detailsPane; + }.bind(this); + } +--> +<dom-module id='tr-ui-a-stacked-pane-view'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + + #pane_container > * { + flex: 0 0 auto; + } + </style> + <div id="pane_container"> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-stacked-pane-view', + + /** + * Add a pane to the stacked pane view. This method performs two operations: + * + * 1. Remove existing descendant panes + * If the optional parent pane is provided, all its current descendant + * panes are removed. Otherwise, all panes are removed from the view. + * + * 2. Build and add new pane + * If a pane builder is provided and returns a pane, the new pane is + * appended to the view (after the provided parent, or at the top). + */ + setPaneBuilder(paneBuilder, opt_parentPane) { + const paneContainer = this.$.pane_container; + + // If the parent pane is provided, it must be an HTML element and a child + // of the pane container. + if (opt_parentPane) { + if (!(opt_parentPane instanceof HTMLElement)) { + throw new Error('Parent pane must be an HTML element'); + } + if (opt_parentPane.parentElement !== paneContainer) { + throw new Error('Parent pane must be a child of the pane container'); + } + } + + // Remove all descendants of the parent pane (or all panes if no parent + // pane was specified) in reverse order. + while (Polymer.dom(paneContainer).lastElementChild !== null && + Polymer.dom(paneContainer).lastElementChild !== opt_parentPane) { + const removedPane = Polymer.dom(this.$.pane_container).lastElementChild; + const listener = this.listeners_.get(removedPane); + if (listener === undefined) { + throw new Error('No listener associated with pane'); + } + this.listeners_.delete(removedPane); + removedPane.removeEventListener( + 'request-child-pane-change', listener); + Polymer.dom(paneContainer).removeChild(removedPane); + } + + if (opt_parentPane && opt_parentPane.parentElement !== paneContainer) { + throw new Error('Parent pane was removed from the pane container'); + } + + // This check is performed here (and not at the beginning of the method) + // because undefined pane builder means that the parent pane requested + // having no child pane (e.g. when selection is cleared). + if (!paneBuilder) return; + + const pane = paneBuilder(); + if (!pane) return; + + if (!(pane instanceof HTMLElement)) { + throw new Error('Pane must be an HTML element'); + } + + // Listen for child pane change requests from the newly added pane. + const listener = function(event) { + this.setPaneBuilder(pane.childPaneBuilder, pane); + }.bind(this); + if (!this.listeners_) { + // Instead of initializing the listeners map in a created() callback, + // we do it lazily here so that subclasses could provide their own + // created() callback (Polymer currently doesn't allow calling overriden + // superclass methods in strict mode). + this.listeners_ = new WeakMap(); + } + this.listeners_.set(pane, listener); + pane.addEventListener('request-child-pane-change', listener); + + Polymer.dom(paneContainer).appendChild(pane); + pane.appended(); + }, + + /** + * Request rebuilding all panes in the view. The panes are rebuilt from the + * top to the bottom (so that parent panes could request changing their + * child panes when they're being rebuilt and the newly constructed child + * panes would be rebuilt as well). + */ + rebuild() { + let currentPane = Polymer.dom(this.$.pane_container).firstElementChild; + while (currentPane) { + currentPane.rebuild(); + currentPane = currentPane.nextElementSibling; + } + }, + + // For testing purposes. + get panesForTesting() { + const panes = []; + let currentChild = Polymer.dom(this.$.pane_container).firstElementChild; + while (currentChild) { + panes.push(currentChild); + currentChild = currentChild.nextElementSibling; + } + return panes; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view_test.html new file mode 100644 index 00000000000..ceae19ab0db --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view_test.html @@ -0,0 +1,205 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/stacked_pane.html"> +<link rel="import" href="/tracing/ui/analysis/stacked_pane_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createPaneView() { + return document.createElement('tr-ui-a-stacked-pane-view'); + } + + function createPane(paneId, opt_rebuildPaneCallback, opt_appendedCallback) { + const paneEl = document.createElement('tr-ui-a-stacked-pane'); + paneEl.paneId = paneId; + + const divEl = document.createElement('div'); + Polymer.dom(divEl).textContent = 'Pane ' + paneId; + divEl.style.width = '400px'; + divEl.style.background = '#ccc'; + divEl.style.textAlign = 'center'; + Polymer.dom(paneEl).appendChild(divEl); + + if (opt_rebuildPaneCallback) { + paneEl.onRebuild_ = opt_rebuildPaneCallback; + } + + if (opt_appendedCallback) { + paneEl.appended = opt_appendedCallback; + } + + return paneEl; + } + + function createPaneBuilder(paneId, opt_rebuildPaneCallback, + opt_appendedCallback) { + return createPane.bind( + undefined, paneId, opt_rebuildPaneCallback, opt_appendedCallback); + } + + function assertPanes(paneView, expectedPaneIds) { + const actualPaneIds = paneView.panesForTesting.map(function(pane) { + return pane.paneId; + }); + assert.deepEqual(actualPaneIds, expectedPaneIds); + } + + test('instantiate_empty', function() { + const viewEl = createPaneView(); + viewEl.rebuild(); + assertPanes(viewEl, []); + // Don't add the pane to HTML output because it has zero height. + }); + + test('instantiate_singlePane', function() { + const viewEl = createPaneView(); + + viewEl.setPaneBuilder(createPaneBuilder(1)); + viewEl.rebuild(); + + assertPanes(viewEl, [1]); + this.addHTMLOutput(viewEl); + }); + + test('instantiate_multiplePanes', function() { + const viewEl = createPaneView(); + + viewEl.setPaneBuilder(createPaneBuilder(1)); + viewEl.setPaneBuilder(createPaneBuilder(2), viewEl.panesForTesting[0]); + viewEl.setPaneBuilder(createPaneBuilder(3), viewEl.panesForTesting[1]); + + assertPanes(viewEl, [1, 2, 3]); + this.addHTMLOutput(viewEl); + }); + + test('changePanes', function() { + const viewEl = createPaneView(); + + viewEl.setPaneBuilder(createPaneBuilder(1)); + assertPanes(viewEl, [1]); + + viewEl.setPaneBuilder(null); + assertPanes(viewEl, []); + + viewEl.setPaneBuilder(createPaneBuilder(2)); + assertPanes(viewEl, [2]); + + viewEl.setPaneBuilder(createPaneBuilder(3), viewEl.panesForTesting[0]); + assertPanes(viewEl, [2, 3]); + + viewEl.setPaneBuilder(createPaneBuilder(4), viewEl.panesForTesting[0]); + assertPanes(viewEl, [2, 4]); + + viewEl.setPaneBuilder(createPaneBuilder(5), viewEl.panesForTesting[1]); + assertPanes(viewEl, [2, 4, 5]); + + viewEl.setPaneBuilder(createPaneBuilder(6), viewEl.panesForTesting[2]); + assertPanes(viewEl, [2, 4, 5, 6]); + + viewEl.setPaneBuilder(createPaneBuilder(7), viewEl.panesForTesting[1]); + assertPanes(viewEl, [2, 4, 7]); + + this.addHTMLOutput(viewEl); + }); + + test('childPanes', function() { + const viewEl = createPaneView(); + + viewEl.setPaneBuilder(createPaneBuilder(1)); + assertPanes(viewEl, [1]); + + // Pane 1 requests a child pane 2. + const pane1 = viewEl.panesForTesting[0]; + pane1.childPaneBuilder = createPaneBuilder(2); + assertPanes(viewEl, [1, 2]); + + // Pane 2 requests removing its child pane (nothing happens). + const pane2 = viewEl.panesForTesting[1]; + pane2.childPaneBuilder = undefined; + assertPanes(viewEl, [1, 2]); + + // Pane 2 requests a child pane 3. + pane2.childPaneBuilder = createPaneBuilder(3); + assertPanes(viewEl, [1, 2, 3]); + + // Pane 2 requests a child pane 4 (its previous child pane 3 is removed). + pane2.childPaneBuilder = createPaneBuilder(4); + assertPanes(viewEl, [1, 2, 4]); + + // Pane 1 requests removing its child pane (panes 2 and 4 are removed). + pane1.childPaneBuilder = undefined; + assertPanes(viewEl, [1]); + + // Check that removed panes cannot affect the pane view. + pane2.childPaneBuilder = createPaneBuilder(5); + assertPanes(viewEl, [1]); + + // Pane 1 requests a child pane 6 (check that everything still works). + pane1.childPaneBuilder = createPaneBuilder(6); + assertPanes(viewEl, [1, 6]); + + // Change the top pane to pane 7. + viewEl.setPaneBuilder(createPaneBuilder(7)); + assertPanes(viewEl, [7]); + + // Check that removed panes cannot affect the pane view. + pane1.childPaneBuilder = createPaneBuilder(5); + assertPanes(viewEl, [7]); + }); + + test('rebuild', function() { + const viewEl = createPaneView(); + + const rebuiltPaneIds = []; + const rebuildPaneCallback = function() { + rebuiltPaneIds.push(this.paneId); + }; + + viewEl.setPaneBuilder(createPaneBuilder(1, rebuildPaneCallback)); + viewEl.setPaneBuilder(createPaneBuilder(2, rebuildPaneCallback), + viewEl.panesForTesting[0]); + viewEl.setPaneBuilder(createPaneBuilder(3, rebuildPaneCallback), + viewEl.panesForTesting[1]); + + // Rebuild isn't triggered. + assert.deepEqual(rebuiltPaneIds, []); + + // Rebuild is triggered, but it isn't necessary (all panes are clean). + viewEl.rebuild(); + assert.deepEqual(rebuiltPaneIds, []); + + // All panes are now marked as dirty, but rebuild isn't triggered (it was + // only scheduled). + viewEl.panesForTesting.forEach(function(pane) { + pane.scheduleRebuild_(); + }); + assert.deepEqual(rebuiltPaneIds, []); + + // Finally, rebuild was triggered and the panes are dirty. + viewEl.rebuild(); + assert.deepEqual(rebuiltPaneIds, [1, 2, 3]); + + // Make sure that panes are clean after the previous rebuild. + viewEl.rebuild(); + assert.deepEqual(rebuiltPaneIds, [1, 2, 3]); + }); + + test('appended', function() { + const viewEl = createPaneView(); + let didFireAppended; + + didFireAppended = false; + viewEl.setPaneBuilder(createPaneBuilder(1, undefined, function() { + didFireAppended = true; + })); + assert.isTrue(didFireAppended); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stub_analysis_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stub_analysis_table.html new file mode 100644 index 00000000000..b371eac4cf9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stub_analysis_table.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + function StubAnalysisTable() { + this.ownerDocument_ = document; + this.nodes_ = []; + } + + StubAnalysisTable.prototype = { + __proto__: Object.protoype, + + get ownerDocument() { + return this.ownerDocument_; + }, + + appendChild(node) { + if (node.tagName === 'TFOOT' || node.tagName === 'THEAD' || + node.tagName === 'TBODY') { + node.__proto__ = StubAnalysisTable.prototype; + node.nodes_ = []; + node.ownerDocument_ = document; + } + this.nodes_.push(node); + }, + + get lastNode() { + return this.nodes_.pop(); + }, + + get nodeCount() { + return this.nodes_.length; + } + }; + + return { + StubAnalysisTable, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table.html new file mode 100644 index 00000000000..3c2bfdbd9cf --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-a-user-expectation-related-samples-table'> + <template> + <style> + #table { + flex: 1 1 auto; + align-self: stretch; + font-size: 12px; + } + </style> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-a-user-expectation-related-samples-table', + + ready() { + this.samples_ = []; + this.$.table.tableColumns = [ + { + title: 'Event(s)', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.type; + if (row.tooltip) { + typeEl.title = row.tooltip; + } + return typeEl; + }, + width: '150px' + }, + { + title: 'Link', + width: '100%', + value(row) { + const linkEl = document.createElement('tr-ui-a-analysis-link'); + if (row.name) { + linkEl.setSelectionAndContent(row.selection, row.name); + } else { + linkEl.selection = row.selection; + } + return linkEl; + } + } + ]; + }, + + hasRelatedSamples() { + return (this.samples_ && this.samples_.length > 0); + }, + + set selection(eventSet) { + this.samples_ = []; + const samples = new tr.model.EventSet; + eventSet.forEach(function(ue) { + samples.addEventSet(ue.associatedSamples); + }.bind(this)); + + if (samples.length > 0) { + this.samples_.push({ + type: 'Overlapping samples', + tooltip: 'All samples overlapping the selected user expectation(s).', + selection: samples + }); + } + this.updateContents_(); + }, + + updateContents_() { + const table = this.$.table; + if (this.samples_ && this.samples_.length > 0) { + table.tableRows = this.samples_.slice(); + } else { + table.tableRows = []; + } + table.rebuild(); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table_test.html new file mode 100644 index 00000000000..368f41ce6c6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table_test.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/sample.html"> +<link rel="import" href="/tracing/model/thread_slice.html"> +<link rel="import" + href="/tracing/ui/analysis/user_expectation_related_samples_table.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createModel() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + const node = tr.c.TestUtils.newProfileNodes(m, ['fake']); + const s1 = new tr.model.Sample(1, 'a_1', node, m.t2); + const s2 = new tr.model.Sample(2, 'a_2', node, m.t2); + const s3 = new tr.model.Sample(3, 'a_3', node, m.t2); + const s4 = new tr.model.Sample(4, 'a_4', node, m.t2); + const s5 = new tr.model.Sample(5, 'a_5', node, m.t2); + const s6 = new tr.model.Sample(6, 'a_6', node, m.t2); + m.samples.push(s1, s2, s3, s4, s5, s6); + m.ve = new tr.c.TestUtils.newSliceEx( + {title: 'V8.Execute', start: 0, end: 4, type: tr.model.ThreadSlice}); + m.t2.sliceGroup.pushSlice(m.ve); + m.up = new tr.c.TestUtils.newInteractionRecord(m, 0, 4); + m.up.associatedEvents.push(m.ve); + m.userModel.expectations.push(m.up); + }); + return m; + } + + test('overlappingSamples', function() { + const m = createModel(); + + const viewEl = document.createElement( + 'tr-ui-a-user-expectation-related-samples-table'); + viewEl.selection = new tr.model.EventSet([m.up]); + + let overlappingSamples; + viewEl.$.table.tableRows.forEach(function(row) { + if (row.type === 'Overlapping samples') { + assert.isUndefined(overlappingSamples); + overlappingSamples = row.selection; + } + }); + + const samplesTitles = overlappingSamples.map(function(e) { + return e.title; + }); + assert.sameMembers(samplesTitles, + ['a_1', 'a_2', 'a_3', 'a_4']); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view.html new file mode 100644 index 00000000000..48ba2ac9fa7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.annotations', function() { + /** + * A base class for all annotation views. + * @constructor + */ + function AnnotationView(viewport, annotation) { + } + + AnnotationView.prototype = { + draw(ctx) { + throw new Error('Not implemented'); + } + }; + + return { + AnnotationView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view_test.html new file mode 100644 index 00000000000..3a3b3615f91 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view_test.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/comment_box_annotation.html"> +<link rel="import" href="/tracing/model/location.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/rect_annotation.html"> +<link rel="import" href="/tracing/model/x_marker_annotation.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createPopulatedTimeline() { + const model = new tr.Model(); + const process = model.getOrCreateProcess(1); + const thread = process.getOrCreateThread(2); + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + {title: 'a', start: 80, duration: 50})); + + const timeline = document.createElement('tr-ui-timeline-track-view'); + const vp = new tr.ui.TimelineViewport(timeline); + timeline.model = model; + timeline.style.maxHeight = '600px'; + + return timeline; + } + + test('rectAnnotation', function() { + const fakeYComponents1 = [{stableId: '1.2', yPercentOffset: 0.3}]; + const fakeYComponents2 = [{stableId: '1.2', yPercentOffset: 0.9}]; + const start = new tr.model.Location(50, fakeYComponents1); + const end = new tr.model.Location(100, fakeYComponents2); + const rectAnnotation = new tr.model.RectAnnotation(start, end); + + const timeline = createPopulatedTimeline(); + timeline.model.addAnnotation(rectAnnotation); + this.addHTMLOutput(timeline); + }); + + test('xMarkerAnnotation', function() { + const xMarkerAnnotation = new tr.model.XMarkerAnnotation(90); + + const timeline = createPopulatedTimeline(); + const model = timeline.model; + timeline.model.addAnnotation(xMarkerAnnotation); + this.addHTMLOutput(timeline); + }); + + test('commentBoxAnnotation', function() { + const fakeYComponents = [{stableId: '1.2', yPercentOffset: 0.5}]; + const location = new tr.model.Location(120, fakeYComponents); + const text = 'abc'; + const commentBoxAnnotation = + new tr.model.CommentBoxAnnotation(location, text); + + const timeline = createPopulatedTimeline(); + timeline.model.addAnnotation(commentBoxAnnotation); + this.addHTMLOutput(timeline); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/comment_box_annotation_view.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/comment_box_annotation_view.html new file mode 100644 index 00000000000..5237b9b0f55 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/comment_box_annotation_view.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/annotations/annotation_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.annotations', function() { + /** + * A view of a comment box consisting of a textarea and a line to the + * actual location. + * @extends {AnnotationView} + * @constructor + */ + function CommentBoxAnnotationView(viewport, annotation) { + this.viewport_ = viewport; + this.annotation_ = annotation; + this.textArea_ = undefined; + + this.styleWidth = 250; + this.styleHeight = 50; + this.fontSize = 10; + this.rightOffset = 50; + this.topOffset = 25; + } + + CommentBoxAnnotationView.prototype = { + __proto__: tr.ui.annotations.AnnotationView.prototype, + + removeTextArea() { + Polymer.dom(Polymer.dom(this.textArea_).parentNode).removeChild( + this.textArea_); + }, + + draw(ctx) { + const coords = this.annotation_.location.toViewCoordinates( + this.viewport_); + if (coords.viewX < 0) { + if (this.textArea_) { + this.textArea_.style.visibility = 'hidden'; + } + return; + } + + // Set up textarea element. + if (!this.textArea_) { + this.textArea_ = document.createElement('textarea'); + this.textArea_.style.position = 'absolute'; + this.textArea_.readOnly = true; + this.textArea_.value = this.annotation_.text; + // Set the z-index so that this is shown on top of canvas. + this.textArea_.style.zIndex = 1; + Polymer.dom(Polymer.dom(ctx.canvas).parentNode) + .appendChild(this.textArea_); + } + + this.textArea_.style.width = this.styleWidth + 'px'; + this.textArea_.style.height = this.styleHeight + 'px'; + this.textArea_.style.fontSize = this.fontSize + 'px'; + this.textArea_.style.visibility = 'visible'; + + // Update positions to latest coordinate. + this.textArea_.style.left = + coords.viewX + ctx.canvas.getBoundingClientRect().left + + this.rightOffset + 'px'; + this.textArea_.style.top = + coords.viewY - ctx.canvas.getBoundingClientRect().top - + this.topOffset + 'px'; + + // Draw pointer line from offset to actual location. + ctx.strokeStyle = 'rgb(0, 0, 0)'; + ctx.lineWidth = 2; + ctx.beginPath(); + tr.ui.b.drawLine(ctx, coords.viewX, + coords.viewY - ctx.canvas.getBoundingClientRect().top, + coords.viewX + this.rightOffset, + coords.viewY - this.topOffset - + ctx.canvas.getBoundingClientRect().top); + ctx.stroke(); + } + }; + + return { + CommentBoxAnnotationView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/rect_annotation_view.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/rect_annotation_view.html new file mode 100644 index 00000000000..16e5f920eea --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/rect_annotation_view.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/annotations/annotation_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.annotations', function() { + /** + * A view responsible for drawing a single highlight rectangle box on + * the timeline. + * @extends {AnnotationView} + * @constructor + */ + function RectAnnotationView(viewport, annotation) { + this.viewport_ = viewport; + this.annotation_ = annotation; + } + + RectAnnotationView.prototype = { + __proto__: tr.ui.annotations.AnnotationView.prototype, + + draw(ctx) { + const dt = this.viewport_.currentDisplayTransform; + const startCoords = + this.annotation_.startLocation.toViewCoordinates(this.viewport_); + const endCoords = + this.annotation_.endLocation.toViewCoordinates(this.viewport_); + + // Prevent drawing into the ruler track by clamping the initial Y + // point and the rect's Y size. + let startY = startCoords.viewY - ctx.canvas.getBoundingClientRect().top; + const sizeY = endCoords.viewY - startCoords.viewY; + if (startY + sizeY < 0) { + // In this case sizeY is negative. If final Y is negative, + // overwrite startY so that the rectangle ends at y=0. + startY = sizeY; + } else if (startY < 0) { + startY = 0; + } + + ctx.fillStyle = this.annotation_.fillStyle; + ctx.fillRect(startCoords.viewX, startY, + endCoords.viewX - startCoords.viewX, sizeY); + } + }; + + return { + RectAnnotationView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/x_marker_annotation_view.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/x_marker_annotation_view.html new file mode 100644 index 00000000000..933ea17ca57 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/x_marker_annotation_view.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/annotations/annotation_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.annotations', function() { + /** + * A view that draws a vertical line on the timeline at a specific timestamp. + * @extends {AnnotationView} + * @constructor + */ + function XMarkerAnnotationView(viewport, annotation) { + this.viewport_ = viewport; + this.annotation_ = annotation; + } + + XMarkerAnnotationView.prototype = { + __proto__: tr.ui.annotations.AnnotationView.prototype, + + draw(ctx) { + const dt = this.viewport_.currentDisplayTransform; + const viewX = dt.xWorldToView(this.annotation_.timestamp); + + ctx.beginPath(); + tr.ui.b.drawLine(ctx, viewX, 0, viewX, ctx.canvas.height); + ctx.strokeStyle = this.annotation_.strokeStyle; + ctx.stroke(); + } + }; + + return { + XMarkerAnnotationView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/animation.html b/chromium/third_party/catapult/tracing/tracing/ui/base/animation.html new file mode 100644 index 00000000000..46c62818fc3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/animation.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + /** + * Represents a procedural animation that can be run by an + * tr.ui.b.AnimationController. + * + * @constructor + */ + function Animation() { + } + + Animation.prototype = { + + /** + * Called when an animation has been queued after a running animation. + * + * @return {boolean} True if the animation can take on the responsibilities + * of the running animation. If true, takeOverFor will be called on the + * animation. + * + * This can be used to build animations that accelerate as pairs of them are + * queued. + */ + canTakeOverFor(existingAnimation) { + throw new Error('Not implemented'); + }, + + /** + * Called to take over responsiblities of an existingAnimation. + * + * At this point, the existingAnimation has been ticked one last time, then + * stopped. This animation will be started after this returns and has the + * job of finishing(or transitioning away from) the effect the existing + * animation was trying to accomplish. + */ + takeOverFor(existingAnimation, newStartTimestamp, target) { + throw new Error('Not implemented'); + }, + + start(timestamp, target) { + throw new Error('Not implemented'); + }, + + /** + * Called when an animation is stopped before it finishes. The animation can + * do what it wants here, usually nothing. + * + * @param {Number} timestamp When the animation was stopped. + * @param {Object} target The object being animated. May be undefined, take + * care. + * @param {boolean} willBeTakenOverByAnotherAnimation Whether this animation + * is going to be handed to another animation's takeOverFor function. + */ + didStopEarly(timestamp, target, + willBeTakenOverByAnotherAnimation) { + }, + + /** + * @return {boolean} true if the animation is finished. + */ + tick(timestamp, target) { + throw new Error('Not implemented'); + } + }; + + return { + Animation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller.html new file mode 100644 index 00000000000..a6149cba5fb --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller.html @@ -0,0 +1,144 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/event_target.html"> +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/ui/base/animation.html"> +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + /** + * Manages execution, queueing and blending of tr.ui.b.Animations against + * a single target. + * + * Targets must have a cloneAnimationState() method that returns all the + * animatable states of that target. + * + * @constructor + * @extends {tr.b.EventTarget} + */ + function AnimationController() { + tr.b.EventTarget.call(this); + + this.target_ = undefined; + + this.activeAnimation_ = undefined; + + this.tickScheduled_ = false; + } + + AnimationController.prototype = { + __proto__: tr.b.EventTarget.prototype, + + get target() { + return this.target_; + }, + + set target(target) { + if (this.activeAnimation_) { + throw new Error('Cannot change target while animation is running.'); + } + if (target.cloneAnimationState === undefined || + typeof target.cloneAnimationState !== 'function') { + throw new Error('target must have a cloneAnimationState function'); + } + + this.target_ = target; + }, + + get activeAnimation() { + return this.activeAnimation_; + }, + + get hasActiveAnimation() { + return !!this.activeAnimation_; + }, + + queueAnimation(animation, opt_now) { + if (this.target_ === undefined) { + throw new Error('Cannot queue animations without a target'); + } + + let now; + if (opt_now !== undefined) { + now = opt_now; + } else { + now = window.performance.now(); + } + + if (this.activeAnimation_) { + // Must tick the animation before stopping it case its about to stop, + // and to update the target with its final sets of edits up to this + // point. + const done = this.activeAnimation_.tick(now, this.target_); + if (done) { + this.activeAnimation_ = undefined; + } + } + + if (this.activeAnimation_) { + if (animation.canTakeOverFor(this.activeAnimation_)) { + this.activeAnimation_.didStopEarly(now, this.target_, true); + animation.takeOverFor(this.activeAnimation_, now, this.target_); + } else { + this.activeAnimation_.didStopEarly(now, this.target_, false); + } + } + this.activeAnimation_ = animation; + this.activeAnimation_.start(now, this.target_); + + if (this.tickScheduled_) return; + this.tickScheduled_ = true; + tr.b.requestAnimationFrame(this.tickActiveAnimation_, this); + }, + + cancelActiveAnimation(opt_now) { + if (!this.activeAnimation_) return; + let now; + if (opt_now !== undefined) { + now = opt_now; + } else { + now = window.performance.now(); + } + this.activeAnimation_.didStopEarly(now, this.target_, false); + this.activeAnimation_ = undefined; + }, + + tickActiveAnimation_(frameBeginTime) { + this.tickScheduled_ = false; + if (!this.activeAnimation_) return; + + if (this.target_ === undefined) { + this.activeAnimation_.didStopEarly(frameBeginTime, this.target_, false); + return; + } + + const oldTargetState = this.target_.cloneAnimationState(); + + const done = this.activeAnimation_.tick(frameBeginTime, this.target_); + if (done) { + this.activeAnimation_ = undefined; + } + + if (this.activeAnimation_) { + this.tickScheduled_ = true; + tr.b.requestAnimationFrame(this.tickActiveAnimation_, this); + } + + if (oldTargetState) { + const e = new tr.b.Event('didtick'); + e.oldTargetState = oldTargetState; + this.dispatchEvent(e, false, false); + } + } + }; + + return { + AnimationController, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller_test.html new file mode 100644 index 00000000000..9366ab4db2b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller_test.html @@ -0,0 +1,166 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/animation_controller.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function SimpleAnimation(options) { + this.stopTime = options.stopTime; + + this.startCalled = false; + this.didStopEarlyCalled = false; + this.wasTakenOver = false; + this.tickCount = 0; + } + + SimpleAnimation.prototype = { + __proto__: tr.ui.b.Animation.prototype, + + canTakeOverFor(existingAnimation) { + return false; + }, + + takeOverFor(existingAnimation, newStartTimestamp, target) { + throw new Error('Not implemented'); + }, + + start(timestamp, target) { + this.startCalled = true; + }, + + didStopEarly(timestamp, target, willBeTakenOver) { + this.didStopEarlyCalled = true; + this.wasTakenOver = willBeTakenOver; + }, + + /** + * @return {boolean} true if the animation is finished. + */ + tick(timestamp, target) { + this.tickCount++; + return timestamp >= this.stopTime; + } + }; + + test('cancel', function() { + const target = { + x: 0, + cloneAnimationState() { return {x: this.x}; } + }; + + const controller = new tr.ui.b.AnimationController(); + controller.target = target; + + const animation = new SimpleAnimation({stopTime: 100}); + controller.queueAnimation(animation); + + tr.b.forcePendingRAFTasksToRun(0); + assert.strictEqual(animation.tickCount, 1); + controller.cancelActiveAnimation(); + assert.isFalse(controller.hasActiveAnimation); + assert.isTrue(animation.didStopEarlyCalled); + }); + + test('simple', function() { + const target = { + x: 0, + cloneAnimationState() { return {x: this.x}; } + }; + + const controller = new tr.ui.b.AnimationController(); + controller.target = target; + + const animation = new SimpleAnimation({stopTime: 100}); + controller.queueAnimation(animation); + + tr.b.forcePendingRAFTasksToRun(0); + assert.strictEqual(animation.tickCount, 1); + assert.isTrue(controller.hasActiveAnimation); + + tr.b.forcePendingRAFTasksToRun(100); + assert.strictEqual(animation.tickCount, 2); + assert.isFalse(controller.hasActiveAnimation); + }); + + test('queueTwo', function() { + // Clear all pending rafs so if something is lingering it will blow up here. + tr.b.forcePendingRAFTasksToRun(0); + + const target = { + x: 0, + cloneAnimationState() { return {x: this.x}; } + }; + + const controller = new tr.ui.b.AnimationController(); + controller.target = target; + + const a1 = new SimpleAnimation({stopTime: 100}); + const a2 = new SimpleAnimation({stopTime: 100}); + controller.queueAnimation(a1, 0); + assert.isTrue(a1.startCalled); + controller.queueAnimation(a2, 50); + assert.isTrue(a1.didStopEarlyCalled); + assert.isTrue(a2.startCalled); + + tr.b.forcePendingRAFTasksToRun(150); + assert.isFalse(controller.hasActiveAnimation); + assert.isAbove(a2.tickCount, 0); + }); + + /** + * @constructor + */ + function AnimationThatCanTakeOverForSimpleAnimation() { + this.takeOverForAnimation = undefined; + } + + AnimationThatCanTakeOverForSimpleAnimation.prototype = { + __proto__: tr.ui.b.Animation.prototype, + + + canTakeOverFor(existingAnimation) { + return existingAnimation instanceof SimpleAnimation; + }, + + takeOverFor(existingAnimation, newStartTimestamp, target) { + this.takeOverForAnimation = existingAnimation; + }, + + start(timestamp, target) { + this.startCalled = true; + } + }; + + test('takeOver', function() { + const target = { + x: 0, + cloneAnimationState() { return {x: this.x}; } + }; + + const controller = new tr.ui.b.AnimationController(); + controller.target = target; + + const a1 = new SimpleAnimation({stopTime: 100}); + const a2 = new AnimationThatCanTakeOverForSimpleAnimation(); + controller.queueAnimation(a1, 0); + assert.isTrue(a1.startCalled); + assert.strictEqual(a1.tickCount, 0); + controller.queueAnimation(a2, 10); + assert.isTrue(a1.didStopEarlyCalled); + assert.isTrue(a1.wasTakenOver); + assert.strictEqual(a1.tickCount, 1); + + assert.strictEqual(a1, a2.takeOverForAnimation); + assert.isTrue(a2.startCalled); + + controller.cancelActiveAnimation(); + assert.isFalse(controller.hasActiveAnimation); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart.html new file mode 100644 index 00000000000..1745861cbe4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart.html @@ -0,0 +1,253 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/column_chart.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const BarChart = tr.ui.b.define('bar-chart', tr.ui.b.ColumnChart); + + BarChart.prototype = { + __proto__: tr.ui.b.ColumnChart.prototype, + + decorate() { + super.decorate(); + this.verticalScale_ = undefined; + this.horizontalScale_ = undefined; + this.isWaterfall_ = false; + }, + + updateScales_() { + super.updateScales_(); + this.yScale_.range([this.graphWidth, 0]); + this.xScale_.range([0, this.graphHeight]); + this.verticalScale_ = this.isYLogScale_ ? d3.scale.log(10) : + d3.scale.linear(); + this.verticalScale_.domain(this.xScale_.domain()); + this.verticalScale_.range([this.graphHeight, 0]); + this.horizontalScale_ = d3.scale.linear(); + this.horizontalScale_.domain(this.yScale_.domain()); + this.horizontalScale_.range([0, this.graphWidth]); + }, + + set isWaterfall(waterfall) { + this.isWaterfall_ = waterfall; + if (waterfall) { + this.getDataSeries('hide').color = 'transparent'; + } + this.updateContents_(); + }, + + get isWaterfall() { + return this.isWaterfall_; + }, + + get defaultGraphHeight() { + return Math.max(20, 10 * this.data_.length); + }, + + get defaultGraphWidth() { + return 100; + }, + + get barHeight() { + return this.graphHeight / this.data.length; + }, + + drawBrush_(brushRectsSel) { + brushRectsSel + .attr('x', 0) + .attr('width', this.graphWidth) + .attr('y', d => this.verticalScale_(d.max)) + .attr('height', d => + this.verticalScale_(d.min) - this.verticalScale_(d.max)) + .attr('fill', 'rgb(213, 236, 229)'); + }, + + getDataPointAtChartPoint_(chartPoint) { + const flippedPoint = { + x: this.graphHeight - chartPoint.y, + y: this.graphWidth - chartPoint.x + }; + return super.getDataPointAtChartPoint_(flippedPoint); + }, + + drawXAxis_(xAxis) { + xAxis.attr('transform', 'translate(0,' + this.graphHeight + ')') + .call(d3.svg.axis() + .scale(this.horizontalScale_) + .orient('bottom')); + }, + + get yAxisWidth() { + return this.computeScaleTickWidth_(this.verticalScale_); + }, + + drawYAxis_(yAxis) { + const axisModifier = d3.svg.axis() + .scale(this.verticalScale_) + .orient('left'); + yAxis.call(axisModifier); + }, + + drawHoverValueBox_(rect) { + const rectHoverEvent = new tr.b.Event('rect-mouseenter'); + rectHoverEvent.rect = rect; + this.dispatchEvent(rectHoverEvent); + + if (!this.enableHoverBox || (this.isWaterfall_ && rect.key === 'hide')) { + return; + } + + const seriesKeys = [...this.seriesByKey_.keys()]; + const chartAreaSel = d3.select(this.chartAreaElement); + chartAreaSel.selectAll('.hover').remove(); + let keyWidthPx = 0; + let keyHeightPx = 0; + let xWidthPx = 0; + let xHeightPx = 0; + let groupWidthPx = 0; + let groupHeightPx = 0; + if (seriesKeys.length > 1 && !this.isGrouped && !this.isWaterfall_) { + keyWidthPx = tr.ui.b.getSVGTextSize( + this.chartAreaElement, rect.key).width; + keyHeightPx = this.textHeightPx_; + } + if (this.data.length > 1 && !this.isWaterfall_) { + xWidthPx = tr.ui.b.getSVGTextSize( + this.chartAreaElement, '' + rect.datum.x).width; + xHeightPx = this.textHeightPx_; + } + if (this.isGrouped && rect.datum.group !== undefined) { + groupWidthPx = tr.ui.b.getSVGTextSize( + this.chartAreaElement, rect.datum.group).width; + groupHeightPx = this.textHeightPx_; + } + const valueWidthPx = tr.ui.b.getSVGTextSize( + this.chartAreaElement, rect.value).width; + const valueHeightPx = this.textHeightPx_; + const maxWidthPx = Math.max(keyWidthPx, xWidthPx, + groupWidthPx, valueWidthPx) + 5; + const hoverWidthPx = this.isGrouped ? maxWidthPx : Math.min(maxWidthPx, + Math.max(50, rect.widthPx)); + let hoverTopPx = rect.topPx; + hoverTopPx = Math.min( + hoverTopPx, this.getBoundingClientRect().height - + valueHeightPx); + let hoverLeftPx = rect.leftPx + (rect.widthPx / 2); + hoverLeftPx = Math.max(hoverLeftPx - hoverWidthPx, -this.margin.left); + + chartAreaSel + .append('rect') + .attr('class', 'hover') + .attr('fill', 'white') + .attr('x', hoverLeftPx) + .attr('y', hoverTopPx) + .attr('width', hoverWidthPx) + .attr('height', keyHeightPx + xHeightPx + + valueHeightPx + groupHeightPx); + + if (seriesKeys.length > 1 && !this.isGrouped && !this.isWaterfall_) { + chartAreaSel + .append('text') + .attr('class', 'hover') + .attr('fill', rect.color === 'transparent' ? '#000000' : rect.color) + .attr('x', hoverLeftPx + 2) + .attr('y', hoverTopPx + keyHeightPx - 3) + .text(rect.key); + } + if (this.data.length > 1 && !this.isWaterfall_) { + chartAreaSel + .append('text') + .attr('class', 'hover') + .attr('fill', rect.color === 'transparent' ? '#000000' : rect.color) + .attr('x', hoverLeftPx + 2) + .attr('y', hoverTopPx + keyHeightPx + valueHeightPx - 3) + .text('' + rect.datum.x); + } + if (this.isGrouped && rect.datum.group !== undefined) { + chartAreaSel + .append('text') + .on('mouseleave', () => this.clearHoverValueBox_(rect)) + .attr('class', 'hover') + .attr('fill', rect.color === 'transparent' ? '#000000' : rect.color) + .attr('x', hoverLeftPx + 2) + .attr('y', hoverTopPx + keyHeightPx + xHeightPx + groupHeightPx - 3) + .text(rect.datum.group); + } + chartAreaSel + .append('text') + .attr('class', 'hover') + .attr('fill', rect.color === 'transparent' ? '#000000' : rect.color) + .attr('x', hoverLeftPx + 2) + .attr('y', hoverTopPx + xHeightPx + keyHeightPx + + groupHeightPx + valueHeightPx - 3) + .text(rect.value); + }, + + flipRect_(rect) { + // Flip |rect| around |y=x|. + return { + datum: rect.datum, + index: rect.index, + key: rect.key, + value: rect.value, + color: rect.color, + topPx: this.graphHeight - rect.leftPx - rect.widthPx, + leftPx: this.graphWidth - rect.topPx - rect.heightPx, + widthPx: rect.heightPx, + heightPx: rect.widthPx, + underflow: rect.underflow, + overflow: rect.overflow, + }; + }, + + drawRect_(rect, sel) { + super.drawRect_(this.flipRect_(rect), sel); + }, + + drawUnderflow_(rect, rectsSel) { + let sel = rectsSel.data([rect]); + sel.enter().append('text') + .text('*') + .attr('fill', rect.color) + .attr('x', 0) + .attr('y', this.graphHeight - rect.leftPx + + 3 + (rect.widthPx / 2)); + sel.exit().remove(); + + sel = rectsSel.data([rect]); + sel.enter().append('rect') + .attr('fill', 'rgba(0, 0, 0, 0)') + .attr('x', 0) + .attr('y', this.graphHeight - rect.leftPx - rect.widthPx) + .attr('width', 10) + .attr('height', rect.widthPx) + .on('mouseenter', () => this.drawHoverValueBox_(this.flipRect_(rect))) + .on('mouseleave', () => this.clearHoverValueBox_(rect)); + sel.exit().remove(); + }, + + drawOverflow_(rect, sel) { + sel = sel.data([rect]); + sel.enter().append('text') + .text('*') + .attr('fill', rect.color) + .attr('x', this.graphWidth) + .attr('y', this.graphHeight - rect.leftPx + + 3 + (rect.widthPx / 2)); + sel.exit().remove(); + } + }; + + return { + BarChart, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart_test.html new file mode 100644 index 00000000000..48e5ff778aa --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart_test.html @@ -0,0 +1,195 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/assert_utils.html"> +<link rel="import" href="/tracing/ui/base/bar_chart.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiation_singleSeries', function() { + const chart = new tr.ui.b.BarChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, value: 100}, + {x: 20, value: 110}, + {x: 30, value: 100}, + {x: 40, value: 50} + ]; + }); + + test('instantiation_singleDatum', function() { + const chart = new tr.ui.b.BarChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 0, value: 100}, + ]; + }); + + test('instantiation_stacked', function() { + const chart = new tr.ui.b.BarChart(); + chart.isStacked = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, foo: 10, bar: 5, qux: 7}, + {x: 20, foo: 11, bar: 6, qux: 3}, + {x: 30, foo: 10, bar: 4, qux: 8}, + {x: 40, foo: 5, bar: 1, qux: 2} + ]; + }); + + test('undefined', function() { + const chart = new tr.ui.b.BarChart(); + assert.throws(function() { + chart.data = undefined; + }); + }); + + test('instantiation_twoSeries', function() { + const chart = new tr.ui.b.BarChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, alpha: 100, beta: 50}, + {x: 20, alpha: 110, beta: 75}, + {x: 30, alpha: 100, beta: 125}, + {x: 40, alpha: 50, beta: 125} + ]; + chart.brushedRange = tr.b.math.Range.fromExplicitRange(20, 40); + }); + + test('instantiation_twoSparseSeriesWithFirstValueSparse', function() { + const chart = new tr.ui.b.BarChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, alpha: 20, beta: undefined}, + {x: 20, alpha: undefined, beta: 10}, + {x: 30, alpha: 10, beta: undefined}, + {x: 45, alpha: undefined, beta: 20}, + {x: 50, alpha: 25, beta: 30} + ]; + }); + + test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() { + const chart = new tr.ui.b.BarChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, alpha: 20, beta: 40}, + {x: 20, alpha: undefined, beta: 10}, + {x: 30, alpha: 10, beta: undefined}, + {x: 45, alpha: undefined, beta: 20}, + {x: 50, alpha: 30, beta: undefined} + ]; + }); + + test('instantiation_interactiveBrushing', function() { + const chart = new tr.ui.b.BarChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, value: 50}, + {x: 20, value: 60}, + {x: 30, value: 80}, + {x: 40, value: 20}, + {x: 50, value: 30}, + {x: 60, value: 20}, + {x: 70, value: 15}, + {x: 80, value: 20} + ]; + + let mouseDownX = undefined; + let curMouseX = undefined; + + function updateBrushedRange() { + if (mouseDownX === undefined || (mouseDownX === curMouseX)) { + chart.brushedRange = new tr.b.math.Range(); + return; + } + const r = new tr.b.math.Range(); + r.min = Math.min(mouseDownX, curMouseX); + r.max = Math.max(mouseDownX, curMouseX); + chart.brushedRange = r; + } + + chart.addEventListener('item-mousedown', function(e) { + mouseDownX = e.x; + curMouseX = e.x; + updateBrushedRange(); + }); + chart.addEventListener('item-mousemove', function(e) { + if (e.button === undefined) return; + curMouseX = e.x; + updateBrushedRange(); + }); + chart.addEventListener('item-mouseup', function(e) { + curMouseX = e.x; + updateBrushedRange(); + }); + }); + + test('instantiation_overrideDataRange', function() { + let chart = new tr.ui.b.BarChart(); + chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(10, 90); + this.addHTMLOutput(chart); + chart.data = [ + {x: 0, value: -20}, + {x: 1, value: 100}, + {x: 2, value: -40}, + {x: 3, value: 100}, + ]; + + chart = new tr.ui.b.BarChart(); + chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(-10, 100); + this.addHTMLOutput(chart); + chart.data = [ + {x: 0, value: 0}, + {x: 1, value: 50}, + ]; + }); + + test('instantiation_Waterfall', function() { + const chart = new tr.ui.b.BarChart(); + chart.graphWidth = 300; + chart.graphHeight = 200; + chart.isStacked = true; + chart.isGrouped = true; + chart.isWaterfall = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 0, alpha: 40, group: 'group1' }, + {x: 1, alpha: 30, group: 'group2' }, + {x: 2}, + {x: 3, hide: 40, beta: 55, group: 'group1' }, + {x: 4, hide: 40, beta: 65, group: 'group2' }, + {x: 5}, + {x: 6, hide: 95, omega: 10, group: 'group1' }, + {x: 7, hide: 95, omega: 20, group: 'group2' } + ]; + }); + + test('instantiation_showHoverValuesForTransparentData', function() { + const chart = new tr.ui.b.BarChart(); + chart.graphWidth = 300; + chart.graphHeight = 200; + chart.isStacked = true; + chart.isGrouped = true; + chart.displayXInHover = true; + chart.getDataSeries('alpha').color = 'transparent'; + this.addHTMLOutput(chart); + chart.data = [ + {x: 0, alpha: 40, beta: 32, omega: 13, group: 'group1' }, + {x: 1, alpha: 30, beta: 22, omega: 14, group: 'group2' }, + {x: 2}, + {x: 3, alpha: 55, beta: 35, omega: 15, group: 'group1' }, + {x: 4, alpha: 45, beta: 40, omega: 16, group: 'group2' }, + {x: 5}, + {x: 6, alpha: 50, beta: 10, omega: 17, group: 'group1' }, + {x: 7, alpha: 60, beta: 15, omega: 18, group: 'group2' } + ]; + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/base.html b/chromium/third_party/catapult/tracing/tracing/ui/base/base.html new file mode 100644 index 00000000000..e0ca9e23c6b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/base.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/polymer_preload.html" data-suppress-import-order> + +<!-- +Polymer is imported through third-party HTML files, which means that we have to +manually list all recursive imports. +--> +<link rel="import" href="/components/polymer/polymer-micro.html" data-suppress-import-order> +<link rel="import" href="/components/polymer/polymer-mini.html" data-suppress-import-order> +<link rel="import" href="/components/polymer/polymer.html" data-suppress-import-order> + +<link rel="import" href="/tracing/ui/base/polymer_postload.html" data-suppress-import-order> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart.html new file mode 100644 index 00000000000..9138d1ea8b0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart.html @@ -0,0 +1,135 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/name_column_chart.html"> +<link rel="import" href="/tracing/ui/base/name_line_chart.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const BoxChart = tr.ui.b.define('box-chart', tr.ui.b.NameLineChart); + + BoxChart.prototype = { + __proto__: tr.ui.b.NameLineChart.prototype, + + get hideLegend() { + return true; + }, + + updateDataRange_() { + if (this.overrideDataRange_ !== undefined) { + return; + } + + this.autoDataRange_.reset(); + for (const datum of this.data_) { + this.autoDataRange_.addValue(datum.percentile_0); + this.autoDataRange_.addValue(datum.percentile_100); + } + }, + + updateScales_() { + super.updateScales_(); + this.xScale_.domain([0, this.data_.length]); + }, + + get xAxisTickOffset() { + return 0.5; + }, + + updateDataRange_() { + if (this.overrideDataRange_ !== undefined) return; + + this.autoDataRange_.reset(); + for (const datum of this.data_) { + this.autoDataRange_.addValue(datum.percentile_0); + this.autoDataRange_.addValue(datum.percentile_100); + } + }, + + updateXAxis_(xAxis) { + xAxis.selectAll('*').remove(); + if (this.hideXAxis) return; + + tr.ui.b.NameColumnChart.prototype.updateXAxis_.call(this, xAxis); + + const baseline = xAxis.selectAll('path').data([this]); + baseline.enter().append('line') + .attr('stroke', 'black') + .attr('x1', this.xScale_(0)) + .attr('x2', this.xScale_(this.data_.length)) + .attr('y1', this.graphHeight) + .attr('y2', this.graphHeight); + baseline.exit().remove(); + }, + + updateDataContents_(dataSel) { + dataSel.selectAll('*').remove(); + const boxesSel = dataSel.selectAll('path'); + for (let index = 0; index < this.data_.length; ++index) { + const datum = this.data_[index]; + const color = datum.color || 'black'; + + // Draw a box between percentiles 25 and 75: + let sel = boxesSel.data([datum]); + sel.enter().append('rect') + .attr('fill', color) + .attr('x', this.xScale_(index + 0.2)) + .attr('width', + this.xScale_(index + 0.8) - this.xScale_(index + 0.2)) + .attr('y', this.yScale_(datum.percentile_75)) + .attr('height', this.yScale_(datum.percentile_25) - + this.yScale_(datum.percentile_75)); + sel.exit().remove(); + + // Draw a horizontal line for percentile_50: + sel = boxesSel.data([datum]); + sel.enter().append('line') + .attr('stroke', color) + .attr('x1', this.xScale_(index)) + .attr('x2', this.xScale_(index + 1)) + .attr('y1', this.yScale_(datum.percentile_50)) + .attr('y2', this.yScale_(datum.percentile_50)); + sel.exit().remove(); + + // Draw two shorter horizontal lines for percentiles 0 and 100: + sel = boxesSel.data([datum]); + sel.enter().append('line') + .attr('stroke', color) + .attr('x1', this.xScale_(index + 0.4)) + .attr('x2', this.xScale_(index + 0.6)) + .attr('y1', this.yScale_(datum.percentile_0)) + .attr('y2', this.yScale_(datum.percentile_0)); + sel.exit().remove(); + sel = boxesSel.data([datum]); + sel.enter().append('line') + .attr('stroke', color) + .attr('x1', this.xScale_(index + 0.4)) + .attr('x2', this.xScale_(index + 0.6)) + .attr('y1', this.yScale_(datum.percentile_100)) + .attr('y2', this.yScale_(datum.percentile_100)); + sel.exit().remove(); + + // Draw a vertical line between percentiles 0 and 100. + sel = boxesSel.data([datum]); + sel.enter().append('line') + .attr('stroke', color) + .attr('x1', this.xScale_(index + 0.5)) + .attr('x2', this.xScale_(index + 0.5)) + .attr('y1', this.yScale_(datum.percentile_100)) + .attr('y2', this.yScale_(datum.percentile_0)); + sel.exit().remove(); + } + } + }; + + return { + BoxChart, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart_test.html new file mode 100644 index 00000000000..da2c7665ace --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart_test.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/box_chart.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiation_singleSeries', function() { + const chart = new tr.ui.b.BoxChart(); + this.addHTMLOutput(chart); + chart.data = [ + { + x: 'a'.repeat(15) + 'A', + percentile_0: 30, + percentile_25: 60, + percentile_50: 110, + percentile_75: 160, + percentile_100: 210, + }, + { + x: 'b'.repeat(10) + 'B', + percentile_0: 0, + percentile_25: 50, + percentile_50: 100, + percentile_75: 150, + percentile_100: 200, + }, + { + x: 'c'.repeat(5) + 'C', + percentile_0: 100, + percentile_25: 150, + percentile_50: 200, + percentile_75: 250, + percentile_100: 300, + }, + ]; + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/camera.html b/chromium/third_party/catapult/tracing/tracing/ui/base/camera.html new file mode 100644 index 00000000000..540f8e1c7ff --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/camera.html @@ -0,0 +1,350 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const deg2rad = tr.b.math.deg2rad; + + const constants = { + DEFAULT_SCALE: 0.5, + DEFAULT_EYE_DISTANCE: 10000, + MINIMUM_DISTANCE: 1000, + MAXIMUM_DISTANCE: 100000, + FOV: 15, + RESCALE_TIMEOUT_MS: 200, + MAXIMUM_TILT: 80, + SETTINGS_NAMESPACE: 'tr.ui_camera' + }; + + const Camera = tr.ui.b.define('camera'); + + Camera.prototype = { + __proto__: HTMLUnknownElement.prototype, + + decorate(eventSource) { + this.eventSource_ = eventSource; + + this.eventSource_.addEventListener('beginpan', + this.onPanBegin_.bind(this)); + this.eventSource_.addEventListener('updatepan', + this.onPanUpdate_.bind(this)); + this.eventSource_.addEventListener('endpan', + this.onPanEnd_.bind(this)); + + this.eventSource_.addEventListener('beginzoom', + this.onZoomBegin_.bind(this)); + this.eventSource_.addEventListener('updatezoom', + this.onZoomUpdate_.bind(this)); + this.eventSource_.addEventListener('endzoom', + this.onZoomEnd_.bind(this)); + + this.eventSource_.addEventListener('beginrotate', + this.onRotateBegin_.bind(this)); + this.eventSource_.addEventListener('updaterotate', + this.onRotateUpdate_.bind(this)); + this.eventSource_.addEventListener('endrotate', + this.onRotateEnd_.bind(this)); + + this.eye_ = [0, 0, constants.DEFAULT_EYE_DISTANCE]; + this.gazeTarget_ = [0, 0, 0]; + this.rotation_ = [0, 0]; + + this.pixelRatio_ = window.devicePixelRatio || 1; + }, + + + get modelViewMatrix() { + const mvMatrix = mat4.create(); + + mat4.lookAt(mvMatrix, this.eye_, this.gazeTarget_, [0, 1, 0]); + return mvMatrix; + }, + + get projectionMatrix() { + const rect = + tr.ui.b.windowRectForElement(this.canvas_). + scaleSize(this.pixelRatio_); + + const aspectRatio = rect.width / rect.height; + const matrix = mat4.create(); + mat4.perspective( + matrix, deg2rad(constants.FOV), aspectRatio, 1, 100000); + + return matrix; + }, + + set canvas(c) { + this.canvas_ = c; + }, + + set deviceRect(rect) { + this.deviceRect_ = rect; + }, + + get stackingDistanceDampening() { + const gazeVector = [ + this.gazeTarget_[0] - this.eye_[0], + this.gazeTarget_[1] - this.eye_[1], + this.gazeTarget_[2] - this.eye_[2]]; + vec3.normalize(gazeVector, gazeVector); + return 1 + gazeVector[2]; + }, + + loadCameraFromSettings(settings) { + this.eye_ = settings.get( + 'eye', this.eye_, constants.SETTINGS_NAMESPACE); + this.gazeTarget_ = settings.get( + 'gaze_target', this.gazeTarget_, constants.SETTINGS_NAMESPACE); + this.rotation_ = settings.get( + 'rotation', this.rotation_, constants.SETTINGS_NAMESPACE); + + this.dispatchRenderEvent_(); + }, + + saveCameraToSettings(settings) { + settings.set( + 'eye', this.eye_, constants.SETTINGS_NAMESPACE); + settings.set( + 'gaze_target', this.gazeTarget_, constants.SETTINGS_NAMESPACE); + settings.set( + 'rotation', this.rotation_, constants.SETTINGS_NAMESPACE); + }, + + resetCamera() { + this.eye_ = [0, 0, constants.DEFAULT_EYE_DISTANCE]; + this.gazeTarget_ = [0, 0, 0]; + this.rotation_ = [0, 0]; + + const settings = tr.b.SessionSettings(); + const keys = settings.keys(constants.SETTINGS_NAMESPACE); + if (keys.length !== 0) { + this.loadCameraFromSettings(settings); + return; + } + + if (this.deviceRect_) { + const rect = tr.ui.b.windowRectForElement(this.canvas_). + scaleSize(this.pixelRatio_); + + this.eye_[0] = this.deviceRect_.width / 2; + this.eye_[1] = this.deviceRect_.height / 2; + + this.gazeTarget_[0] = this.deviceRect_.width / 2; + this.gazeTarget_[1] = this.deviceRect_.height / 2; + } + + this.saveCameraToSettings(settings); + this.dispatchRenderEvent_(); + }, + + updatePanByDelta(delta) { + const rect = + tr.ui.b.windowRectForElement(this.canvas_). + scaleSize(this.pixelRatio_); + + // Get the eye vector, since we'll be adjusting gazeTarget. + const eyeVector = [ + this.eye_[0] - this.gazeTarget_[0], + this.eye_[1] - this.gazeTarget_[1], + this.eye_[2] - this.gazeTarget_[2]]; + const length = vec3.length(eyeVector); + vec3.normalize(eyeVector, eyeVector); + + const halfFov = constants.FOV / 2; + const multiplier = + 2.0 * length * Math.tan(deg2rad(halfFov)) / rect.height; + + // Get the up and right vectors. + const up = [0, 1, 0]; + const rotMatrix = mat4.create(); + mat4.rotate( + rotMatrix, rotMatrix, deg2rad(this.rotation_[1]), [0, 1, 0]); + mat4.rotate( + rotMatrix, rotMatrix, deg2rad(this.rotation_[0]), [1, 0, 0]); + vec3.transformMat4(up, up, rotMatrix); + + const right = [0, 0, 0]; + vec3.cross(right, eyeVector, up); + vec3.normalize(right, right); + + // Update the gaze target. + for (let i = 0; i < 3; ++i) { + this.gazeTarget_[i] += + delta[0] * multiplier * right[i] - delta[1] * multiplier * up[i]; + + this.eye_[i] = this.gazeTarget_[i] + length * eyeVector[i]; + } + + // If we have some z offset, we need to reposition gazeTarget + // to be on the plane z = 0 with normal [0, 0, 1]. + if (Math.abs(this.gazeTarget_[2]) > 1e-6) { + const gazeVector = [-eyeVector[0], -eyeVector[1], -eyeVector[2]]; + const newLength = tr.b.math.clamp( + -this.eye_[2] / gazeVector[2], + constants.MINIMUM_DISTANCE, + constants.MAXIMUM_DISTANCE); + + for (let i = 0; i < 3; ++i) { + this.gazeTarget_[i] = this.eye_[i] + newLength * gazeVector[i]; + } + } + + this.saveCameraToSettings(tr.b.SessionSettings()); + this.dispatchRenderEvent_(); + }, + + updateZoomByDelta(delta) { + let deltaY = delta[1]; + deltaY = tr.b.math.clamp(deltaY, -50, 50); + let scale = 1.0 - deltaY / 100.0; + + const eyeVector = [0, 0, 0]; + vec3.subtract(eyeVector, this.eye_, this.gazeTarget_); + + const length = vec3.length(eyeVector); + + // Clamp the length to allowed values by changing the scale. + if (length * scale < constants.MINIMUM_DISTANCE) { + scale = constants.MINIMUM_DISTANCE / length; + } else if (length * scale > constants.MAXIMUM_DISTANCE) { + scale = constants.MAXIMUM_DISTANCE / length; + } + + vec3.scale(eyeVector, eyeVector, scale); + vec3.add(this.eye_, this.gazeTarget_, eyeVector); + + this.saveCameraToSettings(tr.b.SessionSettings()); + this.dispatchRenderEvent_(); + }, + + updateRotateByDelta(delta) { + delta[0] *= 0.5; + delta[1] *= 0.5; + + if (Math.abs(this.rotation_[0] + delta[1]) > constants.MAXIMUM_TILT) { + return; + } + if (Math.abs(this.rotation_[1] - delta[0]) > constants.MAXIMUM_TILT) { + return; + } + + const eyeVector = [0, 0, 0, 0]; + vec3.subtract(eyeVector, this.eye_, this.gazeTarget_); + + // Undo the current rotation. + const rotMatrix = mat4.create(); + mat4.rotate( + rotMatrix, rotMatrix, -deg2rad(this.rotation_[0]), [1, 0, 0]); + mat4.rotate( + rotMatrix, rotMatrix, -deg2rad(this.rotation_[1]), [0, 1, 0]); + vec4.transformMat4(eyeVector, eyeVector, rotMatrix); + + // Update rotation values. + this.rotation_[0] += delta[1]; + this.rotation_[1] -= delta[0]; + + // Redo the new rotation. + mat4.identity(rotMatrix); + mat4.rotate( + rotMatrix, rotMatrix, deg2rad(this.rotation_[1]), [0, 1, 0]); + mat4.rotate( + rotMatrix, rotMatrix, deg2rad(this.rotation_[0]), [1, 0, 0]); + vec4.transformMat4(eyeVector, eyeVector, rotMatrix); + + vec3.add(this.eye_, this.gazeTarget_, eyeVector); + + this.saveCameraToSettings(tr.b.SessionSettings()); + this.dispatchRenderEvent_(); + }, + + + // Event callbacks. + onPanBegin_(e) { + this.panning_ = true; + this.lastMousePosition_ = this.getMousePosition_(e); + }, + + onPanUpdate_(e) { + if (!this.panning_) return; + + const delta = this.getMouseDelta_(e, this.lastMousePosition_); + this.lastMousePosition_ = this.getMousePosition_(e); + this.updatePanByDelta(delta); + }, + + onPanEnd_(e) { + this.panning_ = false; + }, + + onZoomBegin_(e) { + this.zooming_ = true; + + const p = this.getMousePosition_(e); + + this.lastMousePosition_ = p; + this.zoomPoint_ = p; + }, + + onZoomUpdate_(e) { + if (!this.zooming_) return; + + const delta = this.getMouseDelta_(e, this.lastMousePosition_); + this.lastMousePosition_ = this.getMousePosition_(e); + this.updateZoomByDelta(delta); + }, + + onZoomEnd_(e) { + this.zooming_ = false; + this.zoomPoint_ = undefined; + }, + + onRotateBegin_(e) { + this.rotating_ = true; + this.lastMousePosition_ = this.getMousePosition_(e); + }, + + onRotateUpdate_(e) { + if (!this.rotating_) return; + + const delta = this.getMouseDelta_(e, this.lastMousePosition_); + this.lastMousePosition_ = this.getMousePosition_(e); + this.updateRotateByDelta(delta); + }, + + onRotateEnd_(e) { + this.rotating_ = false; + }, + + + // Misc helper functions. + getMousePosition_(e) { + const rect = tr.ui.b.windowRectForElement(this.canvas_); + return [(e.clientX - rect.x) * this.pixelRatio_, + (e.clientY - rect.y) * this.pixelRatio_]; + }, + + getMouseDelta_(e, p) { + const newP = this.getMousePosition_(e); + return [newP[0] - p[0], newP[1] - p[1]]; + }, + + dispatchRenderEvent_() { + tr.b.dispatchSimpleEvent(this, 'renderrequired', false, false); + } + }; + + return { + Camera, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/camera_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/camera_test.html new file mode 100644 index 00000000000..7083856c539 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/camera_test.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/math/bbox2.html"> +<link rel="import" href="/tracing/base/math/quad.html"> +<link rel="import" href="/tracing/base/math/rect.html"> +<link rel="import" href="/tracing/ui/base/quad_stack_view.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createQuads() { + const quads = [ + tr.b.math.Quad.fromXYWH(-500, -500, 30, 30), // 4 corners + tr.b.math.Quad.fromXYWH(-500, 470, 30, 30), + tr.b.math.Quad.fromXYWH(470, -500, 30, 30), + tr.b.math.Quad.fromXYWH(470, 470, 30, 30), + tr.b.math.Quad.fromXYWH(-250, -250, 250, 250), // crosshairs + tr.b.math.Quad.fromXYWH(0, -250, 250, 250), // crosshairs + tr.b.math.Quad.fromXYWH(-250, 0, 250, 250), // crosshairs + tr.b.math.Quad.fromXYWH(0, 0, 250, 250) // crosshairs + ]; + quads[0].stackingGroupId = 0; + quads[1].stackingGroupId = 0; + quads[2].stackingGroupId = 0; + quads[3].stackingGroupId = 0; + quads[4].stackingGroupId = 1; + quads[5].stackingGroupId = 1; + quads[6].stackingGroupId = 1; + quads[7].stackingGroupId = 1; + return quads; + } + + function createQuadStackView(testFramework) { + const quads = createQuads(); + const view = new tr.ui.b.QuadStackView(); + // simulate the constraints of the layer-tree-view + view.style.height = '400px'; + view.style.width = '800px'; + view.deviceRect = tr.b.math.Rect.fromXYWH(-250, -250, 500, 500); + view.quads = quads; + + testFramework.addHTMLOutput(view); + return view; + } + + test('initialState', function() { + const view = createQuadStackView(this); + + const viewRect = + view.getBoundingClientRect(); + assert.strictEqual(viewRect.height, 400); + assert.strictEqual(viewRect.width, 800); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base.html b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base.html new file mode 100644 index 00000000000..3d1359fe61b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base.html @@ -0,0 +1,453 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/ui/base/chart_legend_key.html"> +<link rel="import" href="/tracing/ui/base/d3.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<template id="chart-base-template"> + <svg> <!-- svg tag is dropped by ChartBase.decorate. --> + <g xmlns="http://www.w3.org/2000/svg" id="chart-area"> + <g class="x axis"></g> + <g class="y axis"></g> + <text id="title"></text> + </g> + </svg> +</template> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const DataSeriesEnableChangeEventType = 'data-series-enabled-change'; + + const THIS_DOC = document.currentScript.ownerDocument; + + const svgNS = 'http://www.w3.org/2000/svg'; + const ColorScheme = tr.b.ColorScheme; + + function getColorOfKey(key, selected) { + let id = ColorScheme.getColorIdForGeneralPurposeString(key); + if (selected) { + id += ColorScheme.properties.brightenedOffsets[0]; + } + return ColorScheme.colorsAsStrings[id]; + } + + /** + * Returns width and height of SVG text node. + * + * @param {!Element} parentNode + * @param {string} text + * @param {function(!Element)=} opt_callback + * @param {*=} opt_this + * @returns {!Object} + */ + function getSVGTextSize(parentNode, text, opt_callback, opt_this) { + const textNode = document.createElementNS( + 'http://www.w3.org/2000/svg', 'text'); + textNode.setAttributeNS(null, 'x', 0); + textNode.setAttributeNS(null, 'y', 0); + textNode.setAttributeNS(null, 'fill', 'black'); + textNode.appendChild(document.createTextNode(text)); + parentNode.appendChild(textNode); + if (opt_callback) { + opt_callback.call(opt_this || parentNode, textNode); + } + const width = textNode.getComputedTextLength(); + const height = textNode.getBBox().height; + parentNode.removeChild(textNode); + return {width, height}; + } + + function DataSeries(key) { + this.key_ = key; + this.target_ = undefined; + this.title_ = ''; + this.optional_ = false; + this.enabled_ = true; + this.color_ = getColorOfKey(key, false); + this.highlightedColor_ = getColorOfKey(key, true); + } + + DataSeries.prototype = { + get key() { + return this.key_; + }, + + get title() { + return this.title_; + }, + + set title(t) { + this.title_ = t; + }, + + get color() { + return this.color_; + }, + + set color(c) { + this.color_ = c; + }, + + get highlightedColor() { + return this.highlightedColor_; + }, + + set highlightedColor(c) { + this.highlightedColor_ = c; + }, + + get optional() { + return this.optional_; + }, + + set optional(optional) { + this.optional_ = optional; + }, + + get enabled() { + return this.enabled_; + }, + + set enabled(enabled) { + // If the caller is disabling a data series, but it wasn't optional, then + // force it to be optional. + if (!this.optional && !enabled) { + this.optional = true; + } + this.enabled_ = enabled; + }, + + get target() { + return this.target_; + }, + + set target(t) { + this.target_ = t; + } + }; + + /** + * A virtual base class for basic charts that provides basic chart + * infrastructure such as a title and legend. + * + * Generally, setting a field on a chart instance will cause it to update its + * contents, which assumes that the chart is attached to a document, so + * callers should create the chart and immediately attach it to a document + * before configuring it. Embedders that are polymer dom-modules can use the + * attached() callback to wait to configure the chart until they are attached + * to a document. + * + * TODO(#3058) Use a class for Polymer 2.0. + * + * @constructor + */ + const ChartBase = tr.ui.b.define('svg', undefined, svgNS); + + ChartBase.prototype = { + __proto__: HTMLUnknownElement.prototype, + + getDataSeries(key) { + if (!this.seriesByKey_.has(key)) { + this.seriesByKey_.set(key, new DataSeries(key)); + } + return this.seriesByKey_.get(key); + }, + + decorate() { + Polymer.dom(this).classList.add('chart-base'); + this.setAttribute('style', 'cursor: default; user-select: none;'); + this.chartTitle_ = undefined; + this.seriesByKey_ = new Map(); + this.graphWidth_ = undefined; + this.graphHeight_ = undefined; + this.margin = { + top: 0, + right: 0, + bottom: 0, + left: 0, + }; + this.hideLegend_ = false; + this.showTitleInLegend_ = false; + this.titleHeight_ = '16pt'; + + // This should use tr.ui.b.instantiateTemplate. However, creating + // svg-namespaced elements inside a template isn't possible. Thus, this + // hack. + const template = + Polymer.dom(THIS_DOC).querySelector('#chart-base-template'); + const svgEl = Polymer.dom(template.content).querySelector('svg'); + for (let i = 0; i < Polymer.dom(svgEl).children.length; i++) { + Polymer.dom(this).appendChild( + Polymer.dom(svgEl.children[i]).cloneNode(true)); + } + + this.addEventListener(DataSeriesEnableChangeEventType, + this.onDataSeriesEnableChange_.bind(this)); + }, + + get hideLegend() { + return this.hideLegend_; + }, + + set hideLegend(h) { + this.hideLegend_ = h; + this.updateContents_(); + }, + + get showTitleInLegend() { + return this.showTitleInLegend_; + }, + + set showTitleInLegend(s) { + this.showTitleInLegend_ = s; + this.updateContents_(); + }, + + isSeriesEnabled(key) { + return this.getDataSeries(key).enabled; + }, + + onDataSeriesEnableChange_(event) { + this.getDataSeries(event.key).enabled = event.enabled; + this.updateContents_(); + }, + + get chartTitle() { + return this.chartTitle_; + }, + + set chartTitle(chartTitle) { + this.chartTitle_ = chartTitle; + this.updateContents_(); + }, + + get chartAreaElement() { + return Polymer.dom(this).querySelector('#chart-area'); + }, + + get graphWidth() { + if (this.graphWidth_ === undefined) return this.defaultGraphWidth; + return this.graphWidth_; + }, + + set graphWidth(width) { + this.graphWidth_ = width; + this.updateContents_(); + }, + + get defaultGraphWidth() { + return 0; + }, + + get graphHeight() { + if (this.graphHeight_ === undefined) return this.defaultGraphHeight; + return this.graphHeight_; + }, + + set graphHeight(height) { + this.graphHeight_ = height; + this.updateContents_(); + }, + + get titleHeight() { + return this.titleHeight_; + }, + + set titleHeight(height) { + this.titleHeight_ = height; + this.updateContents_(); + }, + + get defaultGraphHeight() { + return 0; + }, + + get totalWidth() { + return this.margin.left + this.graphWidth + this.margin.right; + }, + + get totalHeight() { + return this.margin.top + this.graphHeight + this.margin.bottom; + }, + + updateMargins_() { + const legendSize = this.computeLegendSize_(); + this.margin.right = Math.max(this.margin.right, legendSize.width); + this.margin.bottom = Math.max( + this.margin.bottom, + legendSize.height - this.graphHeight); + + if (this.chartTitle_) { + const titleSize = getSVGTextSize(this, this.chartTitle_, textNode => { + textNode.style.fontSize = '16pt'; + }); + this.margin.top = Math.max(this.margin.top, titleSize.height + 15); + const horizontalOverhangPx = (titleSize.width - this.graphWidth) / 2; + this.margin.left = Math.max(this.margin.left, horizontalOverhangPx); + this.margin.right = Math.max(this.margin.right, horizontalOverhangPx); + } + }, + + computeLegendSize_() { + let width = 0; + let height = 0; + if (this.hideLegend) return {width, height}; + + let series = [...this.seriesByKey_.values()]; + if (this.showTitleInLegend) { + series = series.filter(series => series.title !== ''); + } + + for (const seriesEntry of series) { + const legendText = this.showTitleInLegend ? seriesEntry.title : + seriesEntry.key; + const textSize = getSVGTextSize(this, legendText); + width = Math.max(width, textSize.width + 30); + height += textSize.height; + } + + return {width, height}; + }, + + updateDimensions_() { + const thisSel = d3.select(this); + thisSel.attr('width', this.totalWidth); + thisSel.attr('height', this.totalHeight); + + d3.select(this.chartAreaElement).attr( + 'transform', + 'translate(' + this.margin.left + ', ' + this.margin.top + ')'); + }, + + updateContents_() { + this.updateMargins_(); + this.updateDimensions_(); + this.updateTitle_(); + this.updateLegend_(); + }, + + updateTitle_() { + const titleSel = d3.select(this.chartAreaElement).select('#title'); + if (!this.chartTitle_) { + titleSel.style('display', 'none'); + return; + } + titleSel.attr('transform', 'translate(' + this.graphWidth * 0.5 + ',-15)') + .style('display', undefined) + .style('text-anchor', 'middle') + .style('font-size', this.titleHeight) + .attr('class', 'title') + .attr('width', this.graphWidth) + .text(this.chartTitle_); + }, + + updateLegend_() { + const chartAreaSel = d3.select(this.chartAreaElement); + chartAreaSel.selectAll('.legend').remove(); + if (this.hideLegend) return; + + let series; + let seriesText; + if (this.showTitleInLegend) { + series = [...this.seriesByKey_.values()]. + filter(series => series.title !== ''). + filter(series => series.color !== 'transparent').reverse(); + seriesText = series => series.title; + } else { + series = [...this.seriesByKey_.values()]. + filter(series => series.color !== 'transparent').reverse(); + seriesText = series => series.key; + } + + const legendEntriesSel = chartAreaSel.selectAll('.legend').data(series); + + legendEntriesSel.enter() + .append('foreignObject') + .attr('class', 'legend') + .attr('x', this.graphWidth + 2) + .attr('width', this.margin.right) + .attr('height', 18) + .attr('transform', (series, i) => 'translate(0,' + i * 18 + ')') + .append('xhtml:body') + .style('margin', 0) + .append('tr-ui-b-chart-legend-key') + .property('color', series => + ((this.currentHighlightedLegendKey === series.key) ? + series.highlightedColor : series.color)) + .property('width', this.margin.right) + .property('target', series => series.target) + .property('title', series => series.title) + .property('optional', series => series.optional) + .property('enabled', series => series.enabled) + .text(seriesText); + legendEntriesSel.exit().remove(); + }, + + get highlightedLegendKey() { + return this.highlightedLegendKey_; + }, + + set highlightedLegendKey(highlightedLegendKey) { + this.highlightedLegendKey_ = highlightedLegendKey; + this.updateHighlight_(); + }, + + get currentHighlightedLegendKey() { + if (this.tempHighlightedLegendKey_) { + return this.tempHighlightedLegendKey_; + } + return this.highlightedLegendKey_; + }, + + pushTempHighlightedLegendKey(key) { + if (this.tempHighlightedLegendKey_) { + throw new Error('push cannot nest'); + } + this.tempHighlightedLegendKey_ = key; + this.updateHighlight_(); + }, + + popTempHighlightedLegendKey(key) { + if (this.tempHighlightedLegendKey_ !== key) { + throw new Error('pop cannot happen'); + } + this.tempHighlightedLegendKey_ = undefined; + this.updateHighlight_(); + }, + + updateHighlight_() { + // Update label colors. + const chartAreaSel = d3.select(this.chartAreaElement); + const legendEntriesSel = chartAreaSel.selectAll('.legend'); + const getDataSeries = chart.getDataSeries.bind(chart); + const currentHighlightedLegendKey = chart.currentHighlightedLegendKey; + legendEntriesSel.each(function(key) { + // NOTE: this = legendEntry + const dataSeries = getDataSeries(key); + if (key === currentHighlightedLegendKey) { + this.style.fill = dataSeries.highlightedColor; + this.style.fontWeight = 'bold'; + } else { + this.style.fill = dataSeries.color; + this.style.fontWeight = ''; + } + }); + } + }; + + return { + ChartBase, + DataSeriesEnableChangeEventType, + getColorOfKey, + getSVGTextSize, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d.html b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d.html new file mode 100644 index 00000000000..a17e7b74853 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d.html @@ -0,0 +1,571 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/math.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/chart_base.html"> +<link rel="import" href="/tracing/ui/base/mouse_tracker.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + // This does not include the tick labels. + const D3_Y_AXIS_WIDTH_PX = 9; + + // This includes the tick labels. + const D3_X_AXIS_HEIGHT_PX = 23; + + // For charts with log y-axes, the y-axis tick values may need to be sanitized + // if the data is zero or negative. + function sanitizePower(x, defaultValue) { + if (!isNaN(x) && isFinite(x) && (x !== 0)) return x; + return defaultValue; + } + + const ChartBase2D = tr.ui.b.define('chart-base-2d', tr.ui.b.ChartBase); + + ChartBase2D.prototype = { + __proto__: tr.ui.b.ChartBase.prototype, + + decorate() { + super.decorate(); + Polymer.dom(this).classList.add('chart-base-2d'); + + this.xScale_ = d3.scale.linear(); + this.yScale_ = d3.scale.linear(); + this.isYLogScale_ = false; + this.yLogScaleBase_ = 10; + this.yLogScaleMin_ = undefined; + this.autoDataRange_ = new tr.b.math.Range(); + this.overrideDataRange_ = undefined; + this.hideXAxis_ = false; + this.hideYAxis_ = false; + this.data_ = []; + this.xAxisLabel_ = ''; + this.yAxisLabel_ = ''; + this.textHeightPx_ = 0; + this.unit_ = undefined; + + d3.select(this.chartAreaElement) + .append('g') + .attr('id', 'brushes'); + d3.select(this.chartAreaElement) + .append('g') + .attr('id', 'series'); + + this.addEventListener('mousedown', this.onMouseDown_.bind(this)); + }, + + get yLogScaleBase() { + return this.yLogScaleBase_; + }, + + set yLogScaleBase(b) { + this.yLogScaleBase_ = b; + }, + + get unit() { + return this.unit_; + }, + + set unit(unit) { + this.unit_ = unit; + this.updateContents_(); + }, + + get xAxisLabel() { + return this.xAxisLabel_; + }, + + set xAxisLabel(label) { + this.xAxisLabel_ = label; + }, + + get yAxisLabel() { + return this.yAxisLabel_; + }, + + set yAxisLabel(label) { + this.yAxisLabel_ = label; + }, + + get hideXAxis() { + return this.hideXAxis_; + }, + + set hideXAxis(h) { + this.hideXAxis_ = h; + this.updateContents_(); + }, + + get hideYAxis() { + return this.hideYAxis_; + }, + + set hideYAxis(h) { + this.hideYAxis_ = h; + this.updateContents_(); + }, + + get data() { + return this.data_; + }, + + /** + * Sets the data array for the object + * + * @param {Array} data The data. Each element must be an object, with at + * least an x property. All other properties become series names in the + * chart. The data can be sparse (i.e. every x value does not have to + * contain data for every series). + */ + set data(data) { + if (data === undefined) { + throw new Error('data must be an Array'); + } + + this.data_ = data; + this.updateSeriesKeys_(); + this.updateDataRange_(); + this.updateContents_(); + }, + + set isYLogScale(logScale) { + if (logScale) { + this.yScale_ = d3.scale.log().base(this.yLogScaleBase); + } else { + this.yScale_ = d3.scale.linear(); + } + this.isYLogScale_ = logScale; + }, + + getYScaleMin_() { + return this.isYLogScale_ ? this.yLogScaleMin_ : 0; + }, + + getYScaleDomain_(minValue, maxValue) { + if (this.overrideDataRange_ !== undefined) { + return [this.dataRange.min, this.dataRange.max]; + } + if (this.isYLogScale_) { + return [this.getYScaleMin_(), maxValue]; + } + return [Math.min(minValue, this.getYScaleMin_()), maxValue]; + }, + + getSampleWidth_(data, index, leftSide) { + let leftIndex; + let rightIndex; + if (leftSide) { + leftIndex = Math.max(index - 1, 0); + rightIndex = index; + } else { + leftIndex = index; + rightIndex = Math.min(index + 1, data.length - 1); + } + const leftWidth = this.getXForDatum_(data[index], index) - + this.getXForDatum_(data[leftIndex], leftIndex); + const rightWidth = this.getXForDatum_(data[rightIndex], rightIndex) - + this.getXForDatum_(data[index], index); + return tr.b.math.Statistics.mean([leftWidth, rightWidth]); + }, + + updateSeriesKeys_() { + // Don't clear seriesByKey_; the caller might have put state in it using + // getDataSeries() before setting data. + this.data_.forEach(function(datum) { + Object.keys(datum).forEach(function(key) { + if (this.isDatumFieldSeries_(key)) { + this.getDataSeries(key); + } + }, this); + }, this); + }, + + isDatumFieldSeries_(fieldName) { + return fieldName !== 'x'; + }, + + getXForDatum_(datum, index) { + return datum.x; + }, + + updateMargins_() { + this.margin.left = this.hideYAxis ? 0 : this.yAxisWidth; + this.margin.bottom = this.hideXAxis ? 0 : this.xAxisHeight; + + if (this.hideXAxis && !this.hideYAxis) { + this.margin.bottom = 10; + } + if (this.hideYAxis && !this.hideXAxis) { + this.margin.left = 10; + } + this.margin.top = this.hideYAxis ? 0 : 10; + + if (this.yAxisLabel) { + this.margin.top += this.textHeightPx_; + } + if (this.xAxisLabel) { + this.margin.right = Math.max(this.margin.right, + 16 + tr.ui.b.getSVGTextSize(this, this.xAxisLabel).width); + } + + super.updateMargins_(); + }, + + get xAxisHeight() { + return D3_X_AXIS_HEIGHT_PX; + }, + + computeScaleTickWidth_(scale) { + if (this.data.length === 0) return 0; + + let tickValues = scale.ticks(); + let tickFormat = scale.tickFormat(); + + if (this.isYLogScale_) { + const enclosingPowers = this.dataRange.enclosingPowers(); + tickValues = []; + const maxPower = sanitizePower(enclosingPowers.max, this.yLogScaleBase); + for (let power = sanitizePower(enclosingPowers.min, 1); + power <= maxPower; + power *= this.yLogScaleBase) { + tickValues.push(power); + } + tickFormat = v => v.toString(); + } + + if (this.unit) { + tickFormat = v => this.unit.format(v); + } + + let maxTickWidth = 0; + for (const tickValue of tickValues) { + maxTickWidth = Math.max(maxTickWidth, + tr.ui.b.getSVGTextSize(this, tickFormat(tickValue)).width); + } + + return D3_Y_AXIS_WIDTH_PX + maxTickWidth; + }, + + get yAxisWidth() { + return this.computeScaleTickWidth_(this.yScale_); + }, + + updateScales_() { + if (this.data_.length === 0) return; + + this.xScale_.range([0, this.graphWidth]); + this.xScale_.domain(d3.extent(this.data_, this.getXForDatum_.bind(this))); + + this.yScale_.range([this.graphHeight, 0]); + this.yScale_.domain([this.dataRange.min, this.dataRange.max]); + }, + + updateBrushContents_(brushSel) { + brushSel.selectAll('*').remove(); + }, + + updateXAxis_(xAxis) { + xAxis.selectAll('*').remove(); + xAxis[0][0].style.opacity = 0; + if (this.hideXAxis) return; + + this.drawXAxis_(xAxis); + + const label = xAxis.append('text').attr('class', 'label'); + this.drawXAxisTicks_(xAxis); + this.drawXAxisLabel_(label); + xAxis[0][0].style.opacity = 1; + }, + + drawXAxis_(xAxis) { + xAxis.attr('transform', 'translate(0,' + this.graphHeight + ')') + .call(d3.svg.axis() + .scale(this.xScale_) + .orient('bottom')); + }, + + drawXAxisLabel_(label) { + label + .attr('x', this.graphWidth + 16) + .attr('y', 8) + .text(this.xAxisLabel); + }, + + drawXAxisTicks_(xAxis) { + let previousRight = undefined; + xAxis.selectAll('.tick')[0].forEach(function(tick) { + const currentLeft = tick.transform.baseVal[0].matrix.e; + if ((previousRight === undefined) || + (currentLeft > (previousRight + 3))) { + const currentWidth = tick.getBBox().width; + previousRight = currentLeft + currentWidth; + } else { + tick.style.opacity = 0; + } + }); + }, + + set overrideDataRange(range) { + this.overrideDataRange_ = range; + }, + + get dataRange() { + if (this.overrideDataRange_ !== undefined) { + return this.overrideDataRange_; + } + return this.autoDataRange_; + }, + + updateDataRange_() { + if (this.overrideDataRange_ !== undefined) return; + + const dataBySeriesKey = this.getDataBySeriesKey_(); + this.autoDataRange_.reset(); + for (const [series, values] of Object.entries(dataBySeriesKey)) { + for (let i = 0; i < values.length; i++) { + this.autoDataRange_.addValue(values[i][series]); + } + } + + // Choose the closest power of yLogScaleBase, rounded down, as the + // smallest tick to display. + this.yLogScaleMin_ = undefined; + if (this.autoDataRange_.min !== undefined) { + let minValue = this.autoDataRange_.min; + if (minValue === 0) { + minValue = 1; + } + + const onePowerLess = tr.b.math.lesserPower( + minValue / this.yLogScaleBase); + this.yLogScaleMin_ = onePowerLess; + } + }, + + updateYAxis_(yAxis) { + yAxis.selectAll('*').remove(); + yAxis[0][0].style.opacity = 0; + if (this.hideYAxis) return; + + this.drawYAxis_(yAxis); + this.drawYAxisTicks_(yAxis); + + const label = yAxis.append('text').attr('class', 'label'); + this.drawYAxisLabel_(label); + }, + + drawYAxis_(yAxis) { + let axisModifier = d3.svg.axis() + .scale(this.yScale_) + .orient('left'); + + let tickFormat; + + if (this.isYLogScale_) { + if (this.yLogScaleMin_ === undefined) return; + const tickValues = []; + const enclosingPowers = this.dataRange.enclosingPowers(); + const maxPower = sanitizePower(enclosingPowers.max, this.yLogScaleBase); + for (let power = sanitizePower(enclosingPowers.min, 1); + power <= maxPower; + power *= this.yLogScaleBase) { + tickValues.push(power); + } + + // The default tickFormat() for log scales always uses scientific + // notation. Override it to use Number.toString(), which only uses + // scientific notation for extreme values, and uses decimal notation for + // a broader range of values. Decimal notation is generally slightly + // easier to skim than scientific notation in the context of chart axes. + axisModifier = axisModifier.tickValues(tickValues); + tickFormat = v => v.toString(); + } + + if (this.unit) { + tickFormat = v => this.unit.format(v); + } + + if (tickFormat) { + axisModifier = axisModifier.tickFormat(tickFormat); + } + + yAxis.call(axisModifier); + }, + + drawYAxisLabel_(label) { + const labelWidthPx = Math.ceil(tr.ui.b.getSVGTextSize( + this.chartAreaElement, this.yAxisLabel).width); + label + .attr('x', -labelWidthPx) + .attr('y', -8) + .text(this.yAxisLabel); + }, + + drawYAxisTicks_(yAxis) { + let previousTop = undefined; + yAxis.selectAll('.tick')[0].forEach(function(tick) { + const bbox = tick.getBBox(); + const currentTop = tick.transform.baseVal[0].matrix.f; + const currentBottom = currentTop + bbox.height; + if ((previousTop === undefined) || + (previousTop > (currentBottom + 3))) { + previousTop = currentTop; + } else { + tick.style.opacity = 0; + } + }); + yAxis[0][0].style.opacity = 1; + }, + + updateContents_() { + if (this.textHeightPx_ === 0) { + // Measure the height of a string that is as tall as it can be, + // with both an ascender and a descender. + // https://en.wikipedia.org/wiki/Ascender_(typography) + this.textHeightPx_ = tr.ui.b.getSVGTextSize(this, 'Ay').height; + // If the chart is not yet rooted in a document, then the height will be + // 0. Callers should make sure that updateContents_ is called at least + // once after the chart is rooted in a document so that textHeightPx_ + // can be computed. + } + + this.updateScales_(); + super.updateContents_(); + const chartAreaSel = d3.select(this.chartAreaElement); + this.updateXAxis_(chartAreaSel.select('.x.axis')); + this.updateYAxis_(chartAreaSel.select('.y.axis')); + for (const child of this.querySelectorAll('.axis path, .axis line')) { + child.style.fill = 'none'; + child.style.shapeRendering = 'crispEdges'; + child.style.stroke = 'black'; + } + this.updateBrushContents_(chartAreaSel.select('#brushes')); + this.updateDataContents_(chartAreaSel.select('#series')); + }, + + updateDataContents_(seriesSel) { + throw new Error('Not implemented'); + }, + + /** + * Returns a map of series key to the data for that series. + * + * Example: + * // returns {y: [{x: 1, y: 1}, {x: 3, y: 3}], z: [{x: 2, z: 2}]} + * this.data_ = [{x: 1, y: 1}, {x: 2, z: 2}, {x: 3, y: 3}]; + * this.getDataBySeriesKey_(); + * @return {Object} A map of series data by series key. + */ + getDataBySeriesKey_() { + const dataBySeriesKey = {}; + for (const [key, series] of this.seriesByKey_) { + dataBySeriesKey[key] = []; + } + + this.data_.forEach(function(multiSeriesDatum, index) { + const x = this.getXForDatum_(multiSeriesDatum, index); + + d3.keys(multiSeriesDatum).forEach(function(seriesKey) { + // Skip 'x' - it's not a series + if (seriesKey === 'x') return; + + if (multiSeriesDatum[seriesKey] === undefined) return; + + if (!this.isDatumFieldSeries_(seriesKey)) return; + + const singleSeriesDatum = {x}; + singleSeriesDatum[seriesKey] = multiSeriesDatum[seriesKey]; + dataBySeriesKey[seriesKey].push(singleSeriesDatum); + }, this); + }, this); + + return dataBySeriesKey; + }, + + getChartPointAtClientPoint_(clientPoint) { + const rect = this.getBoundingClientRect(); + return { + x: clientPoint.x - rect.left - this.margin.left, + y: clientPoint.y - rect.top - this.margin.top + }; + }, + + getDataPointAtChartPoint_(chartPoint) { + return { + x: tr.b.math.clamp(this.xScale_.invert(chartPoint.x), + this.xScale_.domain()[0], this.xScale_.domain()[1]), + y: tr.b.math.clamp(this.yScale_.invert(chartPoint.y), + this.yScale_.domain()[0], this.yScale_.domain()[1]) + }; + }, + + getDataPointAtClientPoint_(clientX, clientY) { + const chartPoint = this.getChartPointAtClientPoint_( + {x: clientX, y: clientY}); + return this.getDataPointAtChartPoint_(chartPoint); + }, + + prepareDataEvent_(mouseEvent, dataEvent) { + const dataPoint = this.getDataPointAtClientPoint_( + mouseEvent.clientX, mouseEvent.clientY); + dataEvent.x = dataPoint.x; + dataEvent.y = dataPoint.y; + }, + + onMouseDown_(mouseEvent) { + tr.ui.b.trackMouseMovesUntilMouseUp( + this.onMouseMove_.bind(this, mouseEvent.button), + this.onMouseUp_.bind(this, mouseEvent.button)); + mouseEvent.preventDefault(); + mouseEvent.stopPropagation(); + const dataEvent = new tr.b.Event('item-mousedown'); + dataEvent.button = mouseEvent.button; + this.prepareDataEvent_(mouseEvent, dataEvent); + this.dispatchEvent(dataEvent); + for (const child of this.querySelector('#brushes').children) { + child.setAttribute('fill', 'rgb(103, 199, 165)'); + } + }, + + onMouseMove_(button, mouseEvent) { + if (mouseEvent.buttons !== undefined) { + mouseEvent.preventDefault(); + mouseEvent.stopPropagation(); + } + const dataEvent = new tr.b.Event('item-mousemove'); + dataEvent.button = button; + this.prepareDataEvent_(mouseEvent, dataEvent); + this.dispatchEvent(dataEvent); + for (const child of this.querySelector('#brushes').children) { + child.setAttribute('fill', 'rgb(103, 199, 165)'); + } + }, + + onMouseUp_(button, mouseEvent) { + mouseEvent.preventDefault(); + mouseEvent.stopPropagation(); + const dataEvent = new tr.b.Event('item-mouseup'); + dataEvent.button = button; + this.prepareDataEvent_(mouseEvent, dataEvent); + this.dispatchEvent(dataEvent); + for (const child of this.querySelector('#brushes').children) { + child.setAttribute('fill', 'rgb(213, 236, 229)'); + } + } + }; + + return { + ChartBase2D, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d_brushable_x.html b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d_brushable_x.html new file mode 100644 index 00000000000..cba736ee811 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d_brushable_x.html @@ -0,0 +1,90 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/chart_base_2d.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const ChartBase2D = tr.ui.b.ChartBase2D; + const ChartBase2DBrushX = tr.ui.b.define( + 'chart-base-2d-brush-1d', ChartBase2D); + + ChartBase2DBrushX.prototype = { + __proto__: ChartBase2D.prototype, + + decorate() { + super.decorate(); + this.brushedRange_ = new tr.b.math.Range(); + }, + + set brushedRange(range) { + this.brushedRange_.reset(); + this.brushedRange_.addRange(range); + this.updateContents_(); + }, + + get brushedRange() { + return tr.b.math.Range.fromDict(this.brushedRange_.toJSON()); + }, + + computeBrushRangeFromIndices(indexA, indexB) { + indexA = tr.b.math.clamp(indexA, 0, this.data_.length - 1); + indexB = tr.b.math.clamp(indexB, 0, this.data_.length - 1); + const leftIndex = Math.min(indexA, indexB); + const rightIndex = Math.max(indexA, indexB); + + const brushRange = new tr.b.math.Range(); + brushRange.addValue( + this.getXForDatum_(this.data_[leftIndex], leftIndex) - + this.getSampleWidth_(this.data_, leftIndex, true)); + brushRange.addValue( + this.getXForDatum_(this.data_[rightIndex], rightIndex) + + this.getSampleWidth_(this.data_, rightIndex, false)); + return brushRange; + }, + + getDataIndex_(dataX) { + if (this.data.length === 0) return undefined; + const bisect = d3.bisector(this.getXForDatum_.bind(this)).right; + return bisect(this.data_, dataX) - 1; + }, + + prepareDataEvent_(mouseEvent, dataEvent) { + ChartBase2D.prototype.prepareDataEvent_.call( + this, mouseEvent, dataEvent); + dataEvent.index = this.getDataIndex_(dataEvent.x); + if (dataEvent.index !== undefined) { + dataEvent.data = this.data_[dataEvent.index]; + } + }, + + updateBrushContents_(brushSel) { + brushSel.selectAll('*').remove(); + const brushes = this.brushedRange_.isEmpty ? [] : [this.brushedRange_]; + const brushRectsSel = brushSel.selectAll('rect').data(brushes); + brushRectsSel.enter().append('rect'); + brushRectsSel.exit().remove(); + this.drawBrush_(brushRectsSel); + }, + + drawBrush_(brushRectsSel) { + brushRectsSel + .attr('x', d => this.xScale_(d.min)) + .attr('y', 0) + .attr('width', d => this.xScale_(d.max) - this.xScale_(d.min)) + .attr('height', this.graphHeight) + .attr('fill', 'rgb(213, 236, 229)'); + } + }; + + return { + ChartBase2DBrushX, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/chart_legend_key.html b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_legend_key.html new file mode 100644 index 00000000000..1b5b4943898 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_legend_key.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<dom-module id="tr-ui-b-chart-legend-key"> + <template> + <style> + #checkbox { + margin: 0; + visibility: hidden; + vertical-align: text-top; + } + #label, #link { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + display: inline-block; + } + </style> + + <input type=checkbox id="checkbox" checked> + <tr-ui-a-analysis-link id="link"></tr-ui-a-analysis-link> + <label id="label"></label> + </template> +</dom-module> + +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-b-chart-legend-key', + + ready() { + this.$.checkbox.addEventListener( + 'change', this.onCheckboxChange_.bind(this)); + }, + + /** + * Dispatch an event when the checkbox is toggled. + * The checkbox is visible when optional is set to true. + */ + onCheckboxChange_() { + tr.b.dispatchSimpleEvent(this, tr.ui.b.DataSeriesEnableChangeEventType, + true, false, + {key: Polymer.dom(this).textContent, enabled: this.enabled}); + }, + + set textContent(t) { + Polymer.dom(this.$.label).textContent = t; + Polymer.dom(this.$.link).textContent = t; + this.updateContents_(); + }, + + set width(w) { + w -= 20; // reserve 20px for the checkbox + this.$.link.style.width = w + 'px'; + this.$.label.style.width = w + 'px'; + }, + + get textContent() { + return Polymer.dom(this.$.label).textContent; + }, + + /** + * When a legend-key is "optional", then its checkbox is visible to allow + * the user to enable/disable the data series for the key. + * + * @param {boolean} optional + */ + set optional(optional) { + this.$.checkbox.style.visibility = optional ? 'visible' : 'hidden'; + }, + + get optional() { + return this.$.checkbox.style.visibility === 'visible'; + }, + + set enabled(enabled) { + this.$.checkbox.checked = enabled ? 'checked' : ''; + }, + + get enabled() { + return this.$.checkbox.checked; + }, + + set color(c) { + this.$.label.style.color = c; + this.$.link.color = c; + }, + + /** + * When target is defined, label is hidden and link is shown. + * When the link is clicked, then a RequestSelectionChangeEvent is + * dispatched containing the target. + * When target is undefined, label is shown and link is hidden, so that the + * link is not clickable. + */ + set target(target) { + this.$.link.setSelectionAndContent( + target, Polymer.dom(this.$.label).textContent); + this.updateContents_(); + }, + + get target() { + return this.$.link.selection; + }, + + set title(title) { + this.$.link.title = title; + }, + + updateContents_() { + this.$.link.style.display = this.target ? '' : 'none'; + this.$.label.style.display = this.target ? 'none' : ''; + this.$.label.htmlFor = this.optional ? 'checkbox' : ''; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox.html b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox.html new file mode 100644 index 00000000000..0de935ed327 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<dom-module id='tr-ui-b-checkbox'> + <template> + <style> + .inline { + display: inline-block; + } + </style> + + <input type="checkbox" id="checkbox" class="inline"/> + <div id="label" class="inline"></div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-b-checkbox', + + created() { + this.needsInit_ = true; + this.defaultCheckedValue_ = undefined; + this.settingsKey_ = undefined; + this.label_ = undefined; + this.checked_ = false; + this.is_ready_ = false; + }, + + ready() { + this.is_ready_ = true; + this.$.checkbox.addEventListener('click', function() { + this.checked = this.$.checkbox.checked; + }.bind(this)); + this.maybeUpdateElements_(); + }, + + maybeUpdateElements_() { + if (!this.is_ready_) return; + this.$.label.innerText = this.label_; + this.$.checkbox.checked = this.checked_; + }, + + get defaultCheckedValue() { + return this.defaultCheckedValue_; + }, + + set defaultCheckedValue(defaultCheckedValue) { + if (!this.needsInit_) { + throw new Error('Already initialized.'); + } + this.defaultCheckedValue_ = defaultCheckedValue; + this.maybeInit_(); + }, + + get settingsKey() { + return this.settingsKey_; + }, + + set settingsKey(settingsKey) { + if (!this.needsInit_) { + throw new Error('Already initialized.'); + } + this.settingsKey_ = settingsKey; + this.maybeInit_(); + }, + + maybeInit_() { + if (!this.needsInit_) return; + if (this.settingsKey_ === undefined) return; + if (this.defaultCheckedValue_ === undefined) return; + this.needsInit_ = false; + this.checked = tr.b.Settings.get( + this.settingsKey_, this.defaultCheckedValue_); + }, + + get label() { + return this.label_; + }, + + set label(label) { + this.label_ = label; + this.maybeUpdateElements_(); + }, + + get checked() { + return this.checked_; + }, + + set checked(checked) { + this.checked_ = checked; + this.maybeUpdateElements_(); + tr.b.Settings.set(this.settingsKey_, this.checked_); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker.html new file mode 100644 index 00000000000..63073255379 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker.html @@ -0,0 +1,109 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/checkbox.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<dom-module id='tr-ui-b-checkbox-picker'> + <template> + <style> + #container { + display: flex; + flex-direction: column; + } + </style> + + <div id="container"> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-b-checkbox-picker', + created() { + this.needsInit_ = true; + this.settingsKey_ = undefined; + this.is_ready_ = false; + this.checkboxes_ = undefined; + }, + + ready() { + this.is_ready_ = true; + this.maybeInit_(); + this.maybeRenderCheckboxes_(); + }, + + get settingsKey() { + return this.settingsKey_; + }, + + set settingsKey(settingsKey) { + if (!this.needsInit_) { + throw new Error('Already initialized.'); + } + this.settingsKey_ = settingsKey; + this.maybeInit_(); + }, + + maybeInit_() { + if (!this.needsInit_) return; + if (this.settingsKey_ === undefined) return; + if (this.checkboxes_ === undefined) return; + + this.needsInit_ = false; + + for (const key in this.checkboxes_) { + this.checkboxes_[key].defaultCheckedValue = false; + this.checkboxes_[key].settingsKey = this.settingsKey_ + key; + } + }, + + set items(items) { + this.checkboxes_ = {}; + items.forEach(function(e) { + if (e.key in this.checkboxes_) { + throw new Error(e.key + ' already exists'); + } + const checkboxEl = document.createElement('tr-ui-b-checkbox'); + checkboxEl.label = e.label; + this.checkboxes_[e.key] = checkboxEl; + }.bind(this)); + this.maybeInit_(); + this.maybeRenderCheckboxes_(); + }, + + maybeRenderCheckboxes_() { + if (!this.is_ready_) return; + if (this.checkboxes_ === undefined) return; + for (const key in this.checkboxes_) { + Polymer.dom(this.$.container).appendChild(this.checkboxes_[key]); + } + }, + + selectCheckbox(key) { + if (!(key in this.checkboxes_)) { + throw new Error(key + ' does not exists'); + } + this.checkboxes_[key].checked = true; + }, + + unselectCheckbox(key) { + if (!(key in this.checkboxes_)) { + throw new Error(key + ' does not exists'); + } + this.checkboxes_[key].checked = false; + }, + + get checkedKeys() { + return Object.keys(this.checkboxes_).filter(function(k) { + return this.checkboxes_[k].checked; + }.bind(this)); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker_test.html new file mode 100644 index 00000000000..ad893d6cc51 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker_test.html @@ -0,0 +1,135 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/checkbox_picker.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('basicAllCheckboxUnchecked', function() { + const cp = document.createElement('tr-ui-b-checkbox-picker'); + cp.items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Boeing', label: 'I want to fly'} + ]; + this.addHTMLOutput(cp); + assert.deepEqual(cp.checkedKeys, []); + }); + + test('basicSomeCheckboxChecked', function() { + const cp = document.createElement('tr-ui-b-checkbox-picker'); + cp.items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Honda', label: 'I want to drive Honda'}, + {key: 'Tesla', label: 'I want to drive electric car'}, + ]; + + cp.selectCheckbox('Toyota'); + cp.selectCheckbox('Tesla'); + this.addHTMLOutput(cp); + assert.deepEqual(cp.checkedKeys.sort(), ['Tesla', 'Toyota']); + cp.unselectCheckbox('Toyota'); + assert.deepEqual(cp.checkedKeys, ['Tesla']); + }); + + test('duplicateKeys', function() { + const cp = document.createElement('tr-ui-b-checkbox-picker'); + assert.throws(function() { + cp.items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Honda', label: 'I want to drive Honda'}, + {key: 'Toyota', label: 'I want to drive electric car'}, + ]; + }); + }); + + test('selectAndUnselectNonExistingKey', function() { + const cp = document.createElement('tr-ui-b-checkbox-picker'); + cp.items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Honda', label: 'I want to drive Honda'}, + ]; + assert.throws(function() { + cp.selectCheckbox('Lamborghini'); + }); + assert.throws(function() { + cp.unselectCheckbox('Roll Royce'); + }); + }); + + test('testPersistentStateOneSetSettingsKeyBeforeSettingItems', function() { + const container1 = tr.ui.b.createDiv({textContent: 'Checkbox Picker One'}); + container1.style.border = 'solid'; + const cp = document.createElement('tr-ui-b-checkbox-picker'); + cp.settingsKey = 'checkbox-picker-test-one'; + cp.items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Honda', label: 'I want to drive Honda'}, + {key: 'Tesla', label: 'I want to drive electric car'}, + ]; + cp.selectCheckbox('Toyota'); + cp.selectCheckbox('Tesla'); + Polymer.dom(container1).appendChild(cp); + this.addHTMLOutput(container1); + cp.unselectCheckbox('Tesla'); + assert.deepEqual(cp.checkedKeys, ['Toyota']); + + this.addHTMLOutput(document.createElement('br')); + + const container2 = tr.ui.b.createDiv( + {textContent: + 'Checkbox Picker Two (Same settingsKey as Checkbox Picker One)'}); + container2.style.border = 'solid #0000FF'; + const cp2 = document.createElement('tr-ui-b-checkbox-picker'); + cp2.settingsKey = 'checkbox-picker-test-one'; + cp2.items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Honda', label: 'I want to drive Honda'}, + {key: 'Tesla', label: 'I want to drive electric car'}, + ]; + Polymer.dom(container2).appendChild(cp2); + this.addHTMLOutput(container2); + assert.deepEqual(cp2.checkedKeys, ['Toyota']); + }); + + test('testPersistentStateTwoSetSettingsKeyAfterSettingItems', function() { + const container1 = tr.ui.b.createDiv({textContent: 'Checkbox Picker One'}); + container1.style.border = 'solid'; + const cp = document.createElement('tr-ui-b-checkbox-picker'); + cp.items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Honda', label: 'I want to drive Honda'}, + {key: 'Tesla', label: 'I want to drive electric car'}, + ]; + cp.settingsKey = 'checkbox-picker-test-one'; + cp.selectCheckbox('Toyota'); + cp.selectCheckbox('Tesla'); + Polymer.dom(container1).appendChild(cp); + this.addHTMLOutput(container1); + assert.deepEqual(cp.checkedKeys.sort(), ['Tesla', 'Toyota']); + + this.addHTMLOutput(document.createElement('br')); + + const container2 = tr.ui.b.createDiv( + {textContent: + 'Checkbox Picker Two (Same settingsKey as Checkbox Picker One)'}); + container2.style.border = 'solid #0000FF'; + const cp2 = document.createElement('tr-ui-b-checkbox-picker'); + cp2.items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Honda', label: 'I want to drive Honda'}, + {key: 'Tesla', label: 'I want to drive electric car'}, + ]; + Polymer.dom(container2).appendChild(cp2); + this.addHTMLOutput(container2); + cp2.settingsKey = 'checkbox-picker-test-one'; + assert.deepEqual(cp2.checkedKeys.sort(), ['Tesla', 'Toyota']); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_test.html new file mode 100644 index 00000000000..47ac3492ef5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_test.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/checkbox.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('basicUnchecked', function() { + const checkbox = document.createElement('tr-ui-b-checkbox'); + checkbox.label = 'Yo like pizza?'; + this.addHTMLOutput(checkbox); + assert.strictEqual(checkbox.label, 'Yo like pizza?'); + assert.isFalse(checkbox.checked); + }); + + test('basicChecked', function() { + const checkbox = document.createElement('tr-ui-b-checkbox'); + checkbox.label = 'Yo like cookie?'; + checkbox.checked = true; + this.addHTMLOutput(checkbox); + assert.strictEqual(checkbox.label, 'Yo like cookie?'); + assert.isTrue(checkbox.checked); + }); + + test('testPersistentStateOneSetSettingsKeyBeforeAddToDom', function() { + const checkbox = document.createElement('tr-ui-b-checkbox'); + checkbox.settingsKey = 'checkbox-basic-test-one'; + checkbox.label = 'I like sushi'; + checkbox.defaultCheckedValue = false; + this.addHTMLOutput(checkbox); + assert.isFalse(checkbox.checked); + checkbox.checked = true; + + const checkbox2 = document.createElement('tr-ui-b-checkbox'); + checkbox2.label = 'I like sushi'; + checkbox2.defaultCheckedValue = false; + checkbox2.settingsKey = 'checkbox-basic-test-one'; + this.addHTMLOutput(checkbox2); + assert.isTrue(checkbox2.checked); + }); + + test('testPersistentStateTwoSetSettingsKeyAfterAddToDom', function() { + const checkbox = document.createElement('tr-ui-b-checkbox'); + this.addHTMLOutput(checkbox); + checkbox.label = 'I like Ramen'; + checkbox.settingsKey = 'checkbox-basic-test-two'; + checkbox.defaultCheckedValue = false; + assert.isFalse(checkbox.checked); + checkbox.checked = true; + + const checkbox2 = document.createElement('tr-ui-b-checkbox'); + this.addHTMLOutput(checkbox2); + checkbox2.label = 'I like Ramen'; + checkbox2.defaultCheckedValue = false; + checkbox2.settingsKey = 'checkbox-basic-test-two'; + assert.isTrue(checkbox2.checked); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend.html b/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend.html new file mode 100644 index 00000000000..c082f12f834 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/model/compound_event_selection_state.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> + +<!-- +@fileoverview A component used to display a label and a color square. + +The colored square is typically filled with the color associated with +that label, using the getColorId* methods from base/color_scheme. +--> +<dom-module id='tr-ui-b-color-legend'> + <template> + <style> + :host { + display: inline-block; + } + + #square { + font-size: 150%; /* Make the square bigger. */ + line-height: 0%; /* Prevent the square from increasing legend height. */ + } + </style> + <span id="square"></span> + <span id="label"></span> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-b-color-legend', + + ready() { + const blackSquareCharCode = 9632; + this.$.square.innerText = String.fromCharCode(blackSquareCharCode); + this.label_ = undefined; + + this.compoundEventSelectionState_ = + tr.model.CompoundEventSelectionState.NOT_SELECTED; + }, + + set compoundEventSelectionState(compoundEventSelectionState) { + this.compoundEventSelectionState_ = compoundEventSelectionState; + // TODO(nduca): Adjust appearance based on associated state. + }, + + get label() { + return this.label_; + }, + + set label(label) { + if (label === undefined) { + this.setLabelAndColorId(undefined, undefined); + return; + } + + const colorId = tr.b.ColorScheme.getColorIdForGeneralPurposeString( + label); + this.setLabelAndColorId(label, colorId); + }, + + setLabelAndColorId(label, colorId) { + this.label_ = label; + + Polymer.dom(this.$.label).textContent = ''; + Polymer.dom(this.$.label).appendChild(tr.ui.b.asHTMLOrTextNode(label)); + + if (colorId === undefined) { + this.$.square.style.color = 'initial'; + } else { + this.$.square.style.color = tr.b.ColorScheme.colorsAsStrings[colorId]; + } + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend_test.html new file mode 100644 index 00000000000..b19b3a4b7e5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend_test.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/color_legend.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const CompoundEventSelectionState = tr.model.CompoundEventSelectionState; + + function checkSquareColor(colorLegend, expectedColor) { + assert.strictEqual( + getComputedStyle(colorLegend.$.square).color, expectedColor); + } + + test('noLabelSet', function() { + const colorLegend = document.createElement('tr-ui-b-color-legend'); + this.addHTMLOutput(colorLegend); + checkSquareColor(colorLegend, 'rgb(0, 0, 0)'); + }); + + test('undefinedLabel', function() { + const colorLegend = document.createElement('tr-ui-b-color-legend'); + colorLegend.label = undefined; + this.addHTMLOutput(colorLegend); + checkSquareColor(colorLegend, 'rgb(0, 0, 0)'); + }); + + test('emptyLabel', function() { + const colorLegend = document.createElement('tr-ui-b-color-legend'); + colorLegend.label = ''; + this.addHTMLOutput(colorLegend); + checkSquareColor(colorLegend, 'rgb(255, 161, 161)'); + }); + + test('nonEmptyLabel', function() { + const colorLegend = document.createElement('tr-ui-b-color-legend'); + colorLegend.label = 'Frequency'; + this.addHTMLOutput(colorLegend); + checkSquareColor(colorLegend, 'rgb(255, 133, 236)'); + }); + + test('longLabel', function() { + const colorLegend = document.createElement('tr-ui-b-color-legend'); + colorLegend.label = 'Total memory usage'; + this.addHTMLOutput(colorLegend); + checkSquareColor(colorLegend, 'rgb(150, 193, 255)'); + }); + + test('directlySetColorId', function() { + const colorLegend = document.createElement('tr-ui-b-color-legend'); + colorLegend.setLabelAndColorId('hello_world', 7 /* colorId */); + this.addHTMLOutput(colorLegend); + checkSquareColor(colorLegend, 'rgb(152, 220, 149)'); + }); + + test('directlyProvidedLabelElement', function() { + const colorLegend = document.createElement('tr-ui-b-color-legend'); + colorLegend.setLabelAndColorId( + tr.ui.b.createSpan({textContent: 'hello', + className: 'hello-span'}), + 7 /* colorId */); + this.addHTMLOutput(colorLegend); + checkSquareColor(colorLegend, 'rgb(152, 220, 149)'); + }); + + test('cessObjectSelected', function() { + const colorLegend = document.createElement('tr-ui-b-color-legend'); + colorLegend.label = 'Object selected'; + colorLegend.compoundEventSelectionState = + CompoundEventSelectionState.EVENT_SELECTED; + this.addHTMLOutput(colorLegend); + checkSquareColor(colorLegend, 'rgb(228, 184, 134)'); + }); + + test('cessSomeAssociatedObjectsSelected', function() { + const colorLegend = document.createElement('tr-ui-b-color-legend'); + colorLegend.label = 'Some associated objects selected'; + colorLegend.compoundEventSelectionState = + CompoundEventSelectionState.SOME_ASSOCIATED_EVENTS_SELECTED; + + this.addHTMLOutput(colorLegend); + checkSquareColor(colorLegend, 'rgb(255, 155, 172)'); + }); + + test('cessAllAssociatedObjectsSelected', function() { + const colorLegend = document.createElement('tr-ui-b-color-legend'); + colorLegend.label = 'All associated objects selected'; + colorLegend.compoundEventSelectionState = + CompoundEventSelectionState.ALL_ASSOCIATED_EVENTS_SELECTED; + + this.addHTMLOutput(colorLegend); + checkSquareColor(colorLegend, 'rgb(150, 193, 255)'); + }); + + test('cessObjectAndSomeAssociatedObjectsSelected', function() { + const colorLegend = document.createElement('tr-ui-b-color-legend'); + colorLegend.label = 'Object and some associated objects selected'; + colorLegend.compoundEventSelectionState = + CompoundEventSelectionState.EVENT_AND_SOME_ASSOCIATED_SELECTED; + + this.addHTMLOutput(colorLegend); + checkSquareColor(colorLegend, 'rgb(204, 158, 255)'); + }); + + test('cessObjectAndAllAssociatedObjectsSelected', function() { + const colorLegend = document.createElement('tr-ui-b-color-legend'); + colorLegend.label = 'Object and all associated objects selected'; + colorLegend.compoundEventSelectionState = + CompoundEventSelectionState.EVENT_AND_ALL_ASSOCIATED_SELECTED; + + this.addHTMLOutput(colorLegend); + checkSquareColor(colorLegend, 'rgb(255, 146, 193)'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart.html new file mode 100644 index 00000000000..9d14ce92974 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart.html @@ -0,0 +1,427 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/chart_base_2d_brushable_x.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const ColumnChart = tr.ui.b.define('column-chart', tr.ui.b.ChartBase2DBrushX); + + ColumnChart.prototype = { + __proto__: tr.ui.b.ChartBase2DBrushX.prototype, + + decorate() { + super.decorate(); + + // ColumnChart allows bars to have arbitrary, non-uniform widths. Bars + // need not all be the same width. The width of each bar is automatically + // computed from the bar's x-coordinate and that of the next bar, which + // can not define the width of the last bar. This is the width (in the + // xScale's domain (as opposed to the xScale's range (which is measured in + // pixels))) of the last bar. When there are at least 2 bars, this is + // computed as the average width of the bars. When there is a single bar, + // this must default to a non-zero number so that the width of the only + // bar will not be zero. + this.xCushion_ = 1; + + this.isStacked_ = false; + this.isGrouped_ = false; + + this.enableHoverBox = true; + this.displayXInHover = false; + this.enableToolTip = false; + + this.toolTipCallBack_ = () => {}; + }, + + set toolTipCallBack(callback) { + this.toolTipCallBack_ = callback; + }, + + get toolTipCallBack() { + return this.toolTipCallBack_; + }, + + set isGrouped(grouped) { + this.isGrouped_ = grouped; + if (grouped) { + this.getDataSeries('group').color = 'transparent'; + } + this.updateContents_(); + }, + + get isGrouped() { + return this.isGrouped_; + }, + + set isStacked(stacked) { + this.isStacked_ = true; + this.updateContents_(); + }, + + get isStacked() { + return this.isStacked_; + }, + + get defaultGraphHeight() { + return 100; + }, + + get defaultGraphWidth() { + return 10 * this.data_.length; + }, + + updateScales_() { + if (this.data_.length === 0) return; + + let xDifferences = 0; + let currentX = undefined; + let previousX = undefined; + this.data_.forEach(function(datum, index) { + previousX = currentX; + currentX = this.getXForDatum_(datum, index); + if (previousX !== undefined) { + xDifferences += currentX - previousX; + } + }, this); + + // X. + // Leave a cushion on the right so that the last rect doesn't + // exceed the chart boundaries. The last rect's width is set to the + // average width of the rects, which is chart.width / data.length. + this.xScale_.range([0, this.graphWidth]); + const domain = d3.extent(this.data_, this.getXForDatum_.bind(this)); + if (this.data_.length > 1) { + this.xCushion_ = xDifferences / (this.data_.length - 1); + } + this.xScale_.domain([domain[0], domain[1] + this.xCushion_]); + + // Y. + this.yScale_.range([this.graphHeight, 0]); + this.yScale_.domain(this.getYScaleDomain_( + this.dataRange.min, this.dataRange.max)); + }, + + updateDataRange_() { + if (!this.isStacked) { + super.updateDataRange_(); + return; + } + + this.autoDataRange_.reset(); + this.autoDataRange_.addValue(0); + for (const datum of this.data_) { + let sum = 0; + for (const [key, series] of this.seriesByKey_) { + if (datum[key] === undefined) { + continue; + } else if (this.isGrouped && key === 'group') { + continue; + } + sum += datum[key]; + } + this.autoDataRange_.addValue(sum); + } + }, + + getStackedRectsForDatum_(datum, index) { + const stacks = []; + let bottom = this.yScale_.range()[0]; + let sum = 0; + for (const [key, series] of this.seriesByKey_) { + if (datum[key] === undefined || !this.isSeriesEnabled(key)) { + continue; + } else if (this.isGrouped && key === 'group') { + continue; + } + + sum += this.dataRange.clamp(datum[key]); + const heightPx = bottom - this.yScale_(sum); + bottom -= heightPx; + stacks.push({ + key, + value: datum[key], + color: this.getDataSeries(key).color, + heightPx, + topPx: bottom, + underflow: sum < this.dataRange.min, + overflow: sum > this.dataRange.max, + }); + } + return stacks; + }, + + getRectsForDatum_(datum, index) { + if (this.isStacked) { + return this.getStackedRectsForDatum_(datum, index); + } + + const stacks = []; + for (const [key, series] of this.seriesByKey_) { + if (datum[key] === undefined || !this.isSeriesEnabled(key)) { + continue; + } + + const clampedValue = this.dataRange.clamp(datum[key]); + const topPx = this.yScale_(Math.max( + clampedValue, this.getYScaleMin_())); + stacks.push({ + key, + value: datum[key], + topPx, + heightPx: this.yScale_.range()[0] - topPx, + color: this.getDataSeries(key).color, + underflow: datum[key] < this.dataRange.min, + overflow: datum[key] > this.dataRange.max, + }); + } + stacks.sort(function(a, b) { + return b.topPx - a.topPx; + }); + return stacks; + }, + + drawToolTip_(rect) { + if (!this.enableToolTip) return; + + const chartAreaSel = d3.select(this.chartAreaElement); + chartAreaSel.selectAll('.tooltip').remove(); + + const labelText = 'View Breakdown'; + const labelWidth = tr.ui.b.getSVGTextSize( + this.chartAreaElement, labelText).width + 5; + const labelHeight = this.textHeightPx_; + + const toolTipLeftPx = rect.leftPx + (rect.widthPx / 2); + const toolTipTopPx = rect.topPx; + + chartAreaSel + .append('rect') + .attr('class', 'tooltip') + .attr('fill', 'white') + .attr('opacity', 0.8) + .attr('stroke', 'black') + .attr('x', toolTipLeftPx) + .attr('y', toolTipTopPx) + .attr('width', labelWidth + 5) + .attr('height', labelHeight + 10); + + chartAreaSel + .append('text') + .style('cursor', 'pointer') + .attr('class', 'tooltip') + .on('mousedown', () => this.toolTipCallBack_(rect)) + .attr('fill', 'blue') + .attr('x', toolTipLeftPx + 4) + .attr('y', toolTipTopPx + labelHeight) + .attr('text-decoration', 'underline') + .text(labelText); + }, + + drawHoverValueBox_(rect) { + const rectHoverEvent = new tr.b.Event('rect-mouseenter'); + rectHoverEvent.rect = rect; + this.dispatchEvent(rectHoverEvent); + + if (!this.enableHoverBox) return; + + const seriesKeys = [...this.seriesByKey_.keys()]; + const chartAreaSel = d3.select(this.chartAreaElement); + chartAreaSel.selectAll('.hover').remove(); + let keyWidthPx = 0; + let keyHeightPx = 0; + if (seriesKeys.length > 1 && !this.isGrouped) { + keyWidthPx = tr.ui.b.getSVGTextSize( + this.chartAreaElement, rect.key).width + 5; + keyHeightPx = this.textHeightPx_; + } + + let xLabelWidthPx = 0; + let xLabelHeightPx = 0; + if (this.displayXInHover) { + xLabelWidthPx = tr.ui.b.getSVGTextSize( + this.chartAreaElement, rect.datum.x).width + 5; + xLabelHeightPx = this.textHeightPx_; + } + + let groupWidthPx = 0; + let groupHeightPx = 0; + if (this.isGrouped && rect.datum.group !== undefined) { + groupWidthPx = tr.ui.b.getSVGTextSize( + this.chartAreaElement, rect.datum.group).width + 5; + groupHeightPx = this.textHeightPx_; + } + + let value = rect.value; + if (this.unit) value = this.unit.format(value); + const valueWidthPx = tr.ui.b.getSVGTextSize( + this.chartAreaElement, value).width + 5; + const valueHeightPx = this.textHeightPx_; + + const hoverWidthPx = Math.max(keyWidthPx, valueWidthPx, + xLabelWidthPx, groupWidthPx); + + let hoverLeftPx = rect.leftPx + (rect.widthPx / 2); + hoverLeftPx = Math.max(hoverLeftPx - hoverWidthPx, -this.margin.left); + + const hoverHeightPx = keyHeightPx + valueHeightPx + + xLabelHeightPx + groupHeightPx + 2; + + const topOffSetPx = this.isGrouped ? 36 : 12; + let hoverTopPx = rect.topPx; + hoverTopPx = Math.min( + hoverTopPx, this.getBoundingClientRect().height - + hoverHeightPx - topOffSetPx); + + chartAreaSel + .append('rect') + .attr('class', 'hover') + .on('mouseleave', () => this.clearHoverValueBox_(rect)) + .on('mousedown', this.drawToolTip_.bind(this, rect)) + .attr('fill', 'white') + .attr('stroke', 'black') + .attr('x', hoverLeftPx) + .attr('y', hoverTopPx) + .attr('width', hoverWidthPx) + .attr('height', hoverHeightPx); + + if (seriesKeys.length > 1 && !this.isGrouped) { + chartAreaSel + .append('text') + .attr('class', 'hover') + .on('mouseleave', () => this.clearHoverValueBox_(rect)) + .on('mousedown', this.drawToolTip_.bind(this, rect)) + .attr('fill', rect.color) + .attr('x', hoverLeftPx + 2) + .attr('y', hoverTopPx + keyHeightPx - 2) + .text(rect.key); + } + + if (this.displayXInHover) { + chartAreaSel.append('text') + .attr('class', 'hover') + .on('mouseleave', () => this.clearHoverValueBox_(rect)) + .on('mousedown', this.drawToolTip_.bind(this, rect)) + .attr('fill', rect.color) + .attr('x', hoverLeftPx + 2) + .attr('y', hoverTopPx + keyHeightPx + xLabelHeightPx - 2) + .text(rect.datum.x); + } + + if (this.isGrouped && rect.datum.group !== undefined) { + chartAreaSel.append('text') + .attr('class', 'hover') + .on('mouseleave', () => this.clearHoverValueBox_(rect)) + .on('mousedown', this.drawToolTip_.bind(this, rect)) + .attr('fill', rect.color) + .attr('x', hoverLeftPx + 2) + .attr('y', hoverTopPx + keyHeightPx + + xLabelHeightPx + groupHeightPx - 2) + .text(rect.datum.group); + } + + chartAreaSel + .append('text') + .attr('class', 'hover') + .on('mouseleave', () => this.clearHoverValueBox_(rect)) + .on('mousedown', this.drawToolTip_.bind(this, rect)) + .attr('fill', rect.color) + .attr('x', hoverLeftPx + 2) + .attr('y', hoverTopPx + hoverHeightPx - 2) + .text(value); + }, + + clearHoverValueBox_(rect) { + const event = window.event; + if (event.relatedTarget && + Array.from(event.relatedTarget.classList).includes('hover')) { + return; + } + + const rectHoverEvent = new tr.b.Event('rect-mouseleave'); + rectHoverEvent.rect = rect; + this.dispatchEvent(rectHoverEvent); + + d3.select(this.chartAreaElement).selectAll('.hover').remove(); + }, + + drawRect_(rect, sel) { + sel = sel.data([rect]); + sel.enter().append('rect') + .attr('fill', rect.color) + .attr('x', rect.leftPx) + .attr('y', rect.topPx) + .attr('width', rect.widthPx) + .attr('height', rect.heightPx) + .on('mousedown', this.drawToolTip_.bind(this, rect)) + .on('mouseenter', this.drawHoverValueBox_.bind(this, rect)) + .on('mouseleave', this.clearHoverValueBox_.bind(this, rect)); + sel.exit().remove(); + }, + + drawUnderflow_(rect, sel) { + sel = sel.data([rect]); + sel.enter().append('text') + .text('*') + .attr('fill', rect.color) + .attr('x', rect.leftPx + (rect.widthPx / 2)) + .attr('y', this.graphHeight) + .on('mousedown', this.drawToolTip_.bind(this, rect)) + .on('mouseenter', this.drawHoverValueBox_.bind(this, rect)) + .on('mouseleave', this.clearHoverValueBox_.bind(this, rect)); + sel.exit().remove(); + }, + + drawOverflow_(rect, sel) { + sel = sel.data([rect]); + sel.enter().append('text') + .text('*') + .attr('fill', rect.color) + .attr('x', rect.leftPx + (rect.widthPx / 2)) + .attr('y', 0); + sel.exit().remove(); + }, + + updateDataContents_(dataSel) { + dataSel.selectAll('*').remove(); + const chartAreaSel = d3.select(this.chartAreaElement); + const seriesKeys = [...this.seriesByKey_.keys()]; + const rectsSel = dataSel.selectAll('path'); + this.data_.forEach(function(datum, index) { + const currentX = this.getXForDatum_(datum, index); + let width = undefined; + if (index < (this.data_.length - 1)) { + const nextX = this.getXForDatum_(this.data_[index + 1], index + 1); + width = nextX - currentX; + } else { + width = this.xCushion_; + } + for (const rect of this.getRectsForDatum_(datum, index)) { + rect.datum = datum; + rect.index = index; + rect.leftPx = this.xScale_(currentX); + rect.rightPx = this.xScale_(currentX + width); + rect.widthPx = rect.rightPx - rect.leftPx; + this.drawRect_(rect, rectsSel); + if (rect.underflow) { + this.drawUnderflow_(rect, rectsSel); + } + if (rect.overflow) { + this.drawOverflow_(rect, rectsSel); + } + } + }, this); + } + }; + + return { + ColumnChart, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart_test.html new file mode 100644 index 00000000000..4771b9f60fa --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart_test.html @@ -0,0 +1,276 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/column_chart.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('chartLegendKey', function() { + let key = document.createElement('tr-ui-b-chart-legend-key'); + key.textContent = 'Lorem ipsum dolor sit amet'; + key.color = 'red'; + this.addHTMLOutput(key); + + key = document.createElement('tr-ui-b-chart-legend-key'); + key.textContent = 'ipsum dolor sit amet'; + key.target = 'orange ipsum'; + key.color = 'orange'; + this.addHTMLOutput(key); + + key = document.createElement('tr-ui-b-chart-legend-key'); + key.target = 'brown dolor'; + key.color = 'brown'; + key.textContent = 'dolor sit amet'; + this.addHTMLOutput(key); + }); + + test('instantiation_legendTargets', function() { + const chart = new tr.ui.b.ColumnChart(); + chart.getDataSeries('lorem_ipsum').target = 'lorem_ipsumTarget'; + chart.getDataSeries('lorem_ipsum').title = 'lorem ipsum'; + chart.getDataSeries('qux').target = 'quxTarget'; + chart.getDataSeries('lorem_ipsum').optional = true; + chart.getDataSeries('bar').optional = true; + chart.isStacked = true; + chart.hideXAxis = true; + this.addHTMLOutput(chart); + chart.data = [{x: 0, foo: 3, lorem_ipsum: 5, bar: 1, qux: 2}]; + + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate( + chart, function(element) { + return element.tagName === 'TR-UI-B-CHART-LEGEND-KEY' && + element.textContent === 'lorem_ipsum' && + element.target === 'lorem_ipsumTarget'; + })); + }); + + test('instantiation_singleSeries', function() { + const chart = new tr.ui.b.ColumnChart(); + chart.xAxisLabel = 'ms'; + chart.yAxisLabel = '#'; + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, value: 100}, + {x: 20, value: 110}, + {x: 30, value: 100}, + {x: 40, value: 50} + ]; + }); + + test('instantiation_singleDatum', function() { + const chart = new tr.ui.b.ColumnChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 0, value: 100}, + ]; + }); + + test('instantiation_stacked', function() { + const chart = new tr.ui.b.ColumnChart(); + chart.isStacked = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, foo: 10, bar: 5, qux: 7}, + {x: 20, foo: 11, bar: 6, qux: 3}, + {x: 30, foo: 10, bar: 4, qux: 8}, + {x: 40, foo: 5, bar: 1, qux: 2} + ]; + }); + + test('instantiation_singleSeries_yLogScale', function() { + const chart = new tr.ui.b.ColumnChart(); + chart.isYLogScale = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, value: 100}, + {x: 20, value: 10}, + {x: 30, value: 1}, + {x: 40, value: 0.1}, + {x: 50, value: 0.01}, + {x: 60, value: 0.001} + ]; + }); + + test('undefined', function() { + const chart = new tr.ui.b.ColumnChart(); + assert.throws(function() { + chart.data = undefined; + }); + }); + + test('instantiation_twoSeries', function() { + const chart = new tr.ui.b.ColumnChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, alpha: 100, beta: 50}, + {x: 20, alpha: 110, beta: 75}, + {x: 30, alpha: 100, beta: 125}, + {x: 40, alpha: 50, beta: 125} + ]; + + const r = new tr.b.math.Range(); + r.addValue(20); + r.addValue(40); + chart.brushedRange = r; + }); + + test('instantiation_twoSeries_yLogScale', function() { + const chart = new tr.ui.b.ColumnChart(); + chart.isYLogScale = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, alpha: 100, beta: 50}, + {x: 20, alpha: 110, beta: 75}, + {x: 30, alpha: 100, beta: 125}, + {x: 40, alpha: 50, beta: 125} + ]; + + const r = new tr.b.math.Range(); + r.addValue(20); + r.addValue(40); + chart.brushedRange = r; + }); + + test('instantiation_twoSparseSeriesWithFirstValueSparse', function() { + const chart = new tr.ui.b.ColumnChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, alpha: 20, beta: undefined}, + {x: 20, alpha: undefined, beta: 10}, + {x: 30, alpha: 10, beta: undefined}, + {x: 45, alpha: undefined, beta: 20}, + {x: 50, alpha: 25, beta: 30} + ]; + }); + + test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() { + const chart = new tr.ui.b.ColumnChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, alpha: 20, beta: 40}, + {x: 20, alpha: undefined, beta: 10}, + {x: 30, alpha: 10, beta: undefined}, + {x: 45, alpha: undefined, beta: 20}, + {x: 50, alpha: 30, beta: undefined} + ]; + }); + + test('brushRangeFromIndices', function() { + const chart = new tr.ui.b.ColumnChart(); + const data = [ + {x: 10, value: 50}, + {x: 30, value: 60}, + {x: 70, value: 70}, + {x: 80, value: 80}, + {x: 120, value: 90} + ]; + chart.data = data; + let r = new tr.b.math.Range(); + + // Range min should be 10. + r = chart.computeBrushRangeFromIndices(-2, 1); + assert.strictEqual(r.min, 10); + + // Range max should be 120. + r = chart.computeBrushRangeFromIndices(3, 10); + assert.strictEqual(r.max, 120); + + // Range should be [10, 120] + r = chart.computeBrushRangeFromIndices(-2, 10); + assert.strictEqual(r.min, 10); + assert.strictEqual(r.max, 120); + + // Range should be [20, 100] + r = chart.computeBrushRangeFromIndices(1, 3); + assert.strictEqual(r.min, 20); + assert.strictEqual(r.max, 100); + }); + + test('instantiation_interactiveBrushing', function() { + const chart = new tr.ui.b.ColumnChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, value: 50}, + {x: 20, value: 60}, + {x: 30, value: 80}, + {x: 40, value: 20}, + {x: 50, value: 30}, + {x: 60, value: 20}, + {x: 70, value: 15}, + {x: 80, value: 20} + ]; + + let mouseDownX = undefined; + let curMouseX = undefined; + + function updateBrushedRange() { + if (mouseDownX === undefined || (mouseDownX === curMouseX)) { + chart.brushedRange = new tr.b.math.Range(); + return; + } + const r = new tr.b.math.Range(); + r.min = Math.min(mouseDownX, curMouseX); + r.max = Math.max(mouseDownX, curMouseX); + chart.brushedRange = r; + } + + chart.addEventListener('item-mousedown', function(e) { + mouseDownX = e.x; + curMouseX = e.x; + updateBrushedRange(); + }); + chart.addEventListener('item-mousemove', function(e) { + if (e.button === undefined) return; + curMouseX = e.x; + updateBrushedRange(); + }); + chart.addEventListener('item-mouseup', function(e) { + curMouseX = e.x; + updateBrushedRange(); + }); + }); + + test('overrideDataRange', function() { + let chart = new tr.ui.b.ColumnChart(); + chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(10, 90); + this.addHTMLOutput(chart); + chart.data = [ + {x: 0, value: 0}, + {x: 1, value: 100}, + ]; + + chart = new tr.ui.b.ColumnChart(); + chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(-10, 100); + this.addHTMLOutput(chart); + chart.data = [ + {x: 0, value: 0}, + {x: 1, value: 50}, + ]; + }); + + test('instantiationGrouped', function() { + const chart = new tr.ui.b.ColumnChart(); + chart.graphWidth = 300; + chart.graphHeight = 200; + chart.isStacked = true; + chart.isGrouped = true; + chart.displayXInHover = true; + chart.enableToolTip = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, alpha: 100, beta: 50, group: 'group1' }, + {x: 20, alpha: 110, beta: 75, group: 'group2' }, + {x: 30 }, + {x: 40, alpha: 100, beta: 125, group: 'group1' }, + {x: 50, alpha: 50, beta: 125, group: 'group2' } + ]; + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/constants.html b/chromium/third_party/catapult/tracing/tracing/ui/base/constants.html new file mode 100644 index 00000000000..8c38d0bb1b4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/constants.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const constants = { + HEADING_WIDTH: 250 + }; + + return { + constants, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children.html b/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children.html new file mode 100644 index 00000000000..3ce0d3908b1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/event.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<script> +'use strict'; + +/** + * @fileoverview Container that decorates its children. + */ +tr.exportTo('tr.ui.b', function() { + /** + * @constructor + */ + const ContainerThatDecoratesItsChildren = tr.ui.b.define('div'); + + ContainerThatDecoratesItsChildren.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.observer_ = new WebKitMutationObserver(this.didMutate_.bind(this)); + this.observer_.observe(this, { childList: true }); + + // textContent is a variable on regular HTMLElements. However, we want to + // hook and prevent writes to it. + Object.defineProperty( + this, 'textContent', + { get: undefined, set: this.onSetTextContent_}); + }, + + appendChild(x) { + HTMLDivElement.prototype.appendChild.call(this, x); + this.didMutate_(this.observer_.takeRecords()); + }, + + insertBefore(x, y) { + HTMLDivElement.prototype.insertBefore.call(this, x, y); + this.didMutate_(this.observer_.takeRecords()); + }, + + removeChild(x) { + HTMLDivElement.prototype.removeChild.call(this, x); + this.didMutate_(this.observer_.takeRecords()); + }, + + replaceChild(x, y) { + HTMLDivElement.prototype.replaceChild.call(this, x, y); + this.didMutate_(this.observer_.takeRecords()); + }, + + onSetTextContent_(textContent) { + if (textContent !== '') { + throw new Error('textContent can only be set to \'\'.'); + } + this.clear(); + }, + + clear() { + while (Polymer.dom(this).lastChild) { + HTMLDivElement.prototype.removeChild.call( + this, Polymer.dom(this).lastChild); + } + this.didMutate_(this.observer_.takeRecords()); + }, + + didMutate_(records) { + this.beginDecorating_(); + for (let i = 0; i < records.length; i++) { + const addedNodes = records[i].addedNodes; + if (addedNodes) { + for (let j = 0; j < addedNodes.length; j++) { + this.decorateChild_(addedNodes[j]); + } + } + const removedNodes = records[i].removedNodes; + if (removedNodes) { + for (let j = 0; j < removedNodes.length; j++) { + this.undecorateChild_(removedNodes[j]); + } + } + } + this.doneDecoratingForNow_(); + }, + + decorateChild_(child) { + throw new Error('Not implemented'); + }, + + undecorateChild_(child) { + throw new Error('Not implemented'); + }, + + beginDecorating_() { + }, + + doneDecoratingForNow_() { + } + }; + + return { + ContainerThatDecoratesItsChildren, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children_test.html new file mode 100644 index 00000000000..54417b3ded6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children_test.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/base/container_that_decorates_its_children.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createChild() { + const span = document.createElement('span'); + span.decorated = false; + return span; + } + + /** + * @constructor + */ + const SimpleContainer = tr.ui.b.define( + 'simple-container', tr.ui.b.ContainerThatDecoratesItsChildren); + + SimpleContainer.prototype = { + __proto__: tr.ui.b.ContainerThatDecoratesItsChildren.prototype, + + decorateChild_(child) { + assert.isFalse(child.decorated); + child.decorated = true; + }, + + undecorateChild_(child) { + assert.isTrue(child.decorated); + child.decorated = false; + } + }; + + test('add', function() { + const container = new SimpleContainer(); + Polymer.dom(container).appendChild(createChild()); + Polymer.dom(container).appendChild(createChild()); + Polymer.dom(container).appendChild(createChild()); + assert.isTrue(container.children[0].decorated); + assert.isTrue(container.children[1].decorated); + assert.isTrue(container.children[2].decorated); + }); + + test('clearUsingTextContent', function() { + const c0 = createChild(); + const container = new SimpleContainer(); + Polymer.dom(container).appendChild(c0); + Polymer.dom(container).textContent = ''; + assert.isFalse(c0.decorated); + }); + + test('clear', function() { + const c0 = createChild(); + const container = new SimpleContainer(); + Polymer.dom(container).appendChild(c0); + container.clear(); + assert.isFalse(c0.decorated); + }); + + test('insertNewBefore', function() { + const c0 = createChild(); + const c1 = createChild(); + const container = new SimpleContainer(); + Polymer.dom(container).appendChild(c1); + Polymer.dom(container).insertBefore(c0, c1); + assert.isTrue(c0.decorated); + assert.isTrue(c1.decorated); + }); + + test('insertExistingBefore', function() { + const c0 = createChild(); + const c1 = createChild(); + const container = new SimpleContainer(); + Polymer.dom(container).appendChild(c1); + Polymer.dom(container).appendChild(c0); + Polymer.dom(container).insertBefore(c0, c1); + assert.isTrue(c0.decorated); + assert.isTrue(c1.decorated); + }); + + test('testReplace', function() { + const c0 = createChild(); + const c1 = createChild(); + const container = new SimpleContainer(); + Polymer.dom(container).appendChild(c0); + Polymer.dom(container).replaceChild(c1, c0); + assert.isFalse(c0.decorated); + assert.isTrue(c1.decorated); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/d3.html b/chromium/third_party/catapult/tracing/tracing/ui/base/d3.html new file mode 100644 index 00000000000..bce0554c512 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/d3.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<script src="/tracing/ui/base/d3_preload.js"></script> +<script src="/d3.min.js"></script> +<script src="/tracing/ui/base/d3_postload.js"></script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/d3_postload.js b/chromium/third_party/catapult/tracing/tracing/ui/base/d3_postload.js new file mode 100644 index 00000000000..94cefdb15f9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/d3_postload.js @@ -0,0 +1,8 @@ +/* Copyright (c) 2014 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ +'use strict'; + +(function(window) { + window.define = undefined; +}).call(this, this); diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/d3_preload.js b/chromium/third_party/catapult/tracing/tracing/ui/base/d3_preload.js new file mode 100644 index 00000000000..57548f1d175 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/d3_preload.js @@ -0,0 +1,11 @@ +/* Copyright (c) 2014 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ +'use strict'; + +(function(window) { + window.define = function(x) { + window.d3 = x; + }; + window.define.amd = true; +})(this); diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils.html b/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils.html new file mode 100644 index 00000000000..2a1ad88d6cc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> + +<script> + +'use strict'; + +tr.exportTo('tr.ui.b', function() { + function iterateElementDeeplyImpl(element, cb, thisArg, includeElement) { + if (includeElement && cb.call(thisArg, element)) return true; + + if (element.root && + element.root !== element && + iterateElementDeeplyImpl(element.root, cb, thisArg, false)) { + // Some elements, most notably Polymer template dom-repeat='...' + // elements, are their own shadow root. Make sure that we avoid infinite + // recursion by avoiding these elements. + return true; + } + const children = Polymer.dom(element).children; + for (let i = 0; i < children.length; i++) { + if (iterateElementDeeplyImpl(children[i], cb, thisArg, true)) { + return true; + } + } + + return false; + } + + function iterateElementDeeply(element, cb, thisArg) { + iterateElementDeeplyImpl(element, cb, thisArg, false); + } + + function findDeepElementMatchingPredicate(element, predicate) { + let foundElement = undefined; + function matches(element) { + const match = predicate(element); + if (!match) { + return false; + } + foundElement = element; + return true; + } + iterateElementDeeply(element, matches); + return foundElement; + } + + function findDeepElementsMatchingPredicate(element, predicate) { + const foundElements = []; + function matches(element) { + const match = predicate(element); + if (match) { + foundElements.push(element); + } + return false; + } + iterateElementDeeply(element, matches); + return foundElements; + } + + function findDeepElementMatching(element, selector) { + return findDeepElementMatchingPredicate(element, function(element) { + return element.matches(selector); + }); + } + function findDeepElementsMatching(element, selector) { + return findDeepElementsMatchingPredicate(element, function(element) { + return element.matches(selector); + }); + } + function findDeepElementWithTextContent(element, re) { + return findDeepElementMatchingPredicate(element, function(element) { + if (element.children.length !== 0) return false; + return re.test(Polymer.dom(element).textContent); + }); + } + + return { + findDeepElementMatching, + findDeepElementsMatching, + findDeepElementMatchingPredicate, + findDeepElementsMatchingPredicate, + findDeepElementWithTextContent, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils_test.html new file mode 100644 index 00000000000..d9e1d49f556 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils_test.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/deep_utils.html"> + +<dom-module id='tr-ui-b-deep-utils-test-a'> + <template> + <div></div> + </template> +</dom-module> +<dom-module id='tr-ui-b-deep-utils-test-b'> + <template> + <div></div> + </template> +</dom-module> +<dom-module id='tr-ui-b-deep-utils-test-c'> + <template> + <tr-ui-b-deep-utils-test-b class='x'></tr-ui-b-deep-utils-test-b> + <tr-ui-b-deep-utils-test-a class='x'></tr-ui-b-deep-utils-test-a> + <tr-ui-b-deep-utils-test-a class='x'></tr-ui-b-deep-utils-test-a> + </template> +</dom-module> +<dom-module id='tr-ui-b-deep-utils-test-d'> + <template> + <tr-ui-b-deep-utils-test-c></tr-ui-b-deep-utils-test-c> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-b-deep-utils-test-a' +}); + +Polymer({ + is: 'tr-ui-b-deep-utils-test-b' +}); + +Polymer({ + is: 'tr-ui-b-deep-utils-test-c' +}); + +Polymer({ + is: 'tr-ui-b-deep-utils-test-d' +}); + +tr.b.unittest.testSuite(function() { + test('testFindDeepElementMatching', function() { + const d = document.createElement('tr-ui-b-deep-utils-test-d'); + + const b = tr.ui.b.findDeepElementMatching(d, 'tr-ui-b-deep-utils-test-b.x'); + assert.isDefined(b); + assert.strictEqual(b.tagName, 'TR-UI-B-DEEP-UTILS-TEST-B'); + }); + + test('testFindDeepElementsMatching', function() { + const d = document.createElement('tr-ui-b-deep-utils-test-d'); + + const a = tr.ui.b.findDeepElementsMatching( + d, 'tr-ui-b-deep-utils-test-a.x'); + assert.isDefined(a); + assert.strictEqual(a[0].tagName, 'TR-UI-B-DEEP-UTILS-TEST-A'); + assert.strictEqual(a[1].tagName, 'TR-UI-B-DEEP-UTILS-TEST-A'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers.html b/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers.html new file mode 100644 index 00000000000..adf7f10b34d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers.html @@ -0,0 +1,390 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + function createSpan(opt_dictionary) { + let ownerDocument = document; + if (opt_dictionary && opt_dictionary.ownerDocument) { + ownerDocument = opt_dictionary.ownerDocument; + } + const spanEl = ownerDocument.createElement('span'); + if (opt_dictionary) { + if (opt_dictionary.className) { + spanEl.className = opt_dictionary.className; + } + if (opt_dictionary.textContent) { + Polymer.dom(spanEl).textContent = + opt_dictionary.textContent; + } + if (opt_dictionary.tooltip) { + spanEl.title = opt_dictionary.tooltip; + } + if (opt_dictionary.parent) { + Polymer.dom(opt_dictionary.parent).appendChild(spanEl); + } + if (opt_dictionary.bold) { + spanEl.style.fontWeight = 'bold'; + } + if (opt_dictionary.italic) { + spanEl.style.fontStyle = 'italic'; + } + if (opt_dictionary.marginLeft) { + spanEl.style.marginLeft = opt_dictionary.marginLeft; + } + if (opt_dictionary.marginRight) { + spanEl.style.marginRight = opt_dictionary.marginRight; + } + if (opt_dictionary.backgroundColor) { + spanEl.style.backgroundColor = opt_dictionary.backgroundColor; + } + if (opt_dictionary.color) { + spanEl.style.color = opt_dictionary.color; + } + } + return spanEl; + } + + function createLink(opt_args) { + let ownerDocument = document; + if (opt_args && opt_args.ownerDocument) { + ownerDocument = opt_args.ownerDocument; + } + const linkEl = ownerDocument.createElement('a'); + if (opt_args) { + if (opt_args.href) linkEl.href = opt_args.href; + if (opt_args.tooltip) linkEl.title = opt_args.tooltip; + if (opt_args.color) linkEl.style.color = opt_args.color; + if (opt_args.bold) linkEl.style.fontWeight = 'bold'; + if (opt_args.italic) linkEl.style.fontStyle = 'italic'; + if (opt_args.className) linkEl.className = opt_args.className; + if (opt_args.parent) Polymer.dom(opt_args.parent).appendChild(linkEl); + if (opt_args.marginLeft) linkEl.style.marginLeft = opt_args.marginLeft; + if (opt_args.marginRight) linkEl.style.marginRight = opt_args.marginRight; + if (opt_args.backgroundColor) { + linkEl.style.backgroundColor = opt_args.backgroundColor; + } + if (opt_args.textContent) { + Polymer.dom(linkEl).textContent = opt_args.textContent; + } + } + return linkEl; + } + + function createDiv(opt_dictionary) { + const divEl = document.createElement('div'); + if (opt_dictionary) { + if (opt_dictionary.className) { + divEl.className = opt_dictionary.className; + } + if (opt_dictionary.parent) { + Polymer.dom(opt_dictionary.parent).appendChild(divEl); + } + if (opt_dictionary.textContent) { + Polymer.dom(divEl).textContent = + opt_dictionary.textContent; + } + if (opt_dictionary.maxWidth) { + divEl.style.maxWidth = opt_dictionary.maxWidth; + } + } + return divEl; + } + + function createScopedStyle(styleContent) { + const styleEl = document.createElement('style'); + styleEl.scoped = true; + Polymer.dom(styleEl).innerHTML = styleContent; + return styleEl; + } + + function valuesEqual(a, b) { + if (a instanceof Array && b instanceof Array) { + return a.length === b.length && JSON.stringify(a) === JSON.stringify(b); + } + return a === b; + } + + function createSelector( + targetEl, targetElProperty, + settingsKey, defaultValue, + items, opt_namespace) { + let defaultValueIndex; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (valuesEqual(item.value, defaultValue)) { + defaultValueIndex = i; + break; + } + } + if (defaultValueIndex === undefined) { + throw new Error('defaultValue must be in the items list'); + } + + const selectorEl = document.createElement('select'); + selectorEl.addEventListener('change', onChange); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const optionEl = document.createElement('option'); + Polymer.dom(optionEl).textContent = item.label; + optionEl.targetPropertyValue = item.value; + optionEl.item = item; + Polymer.dom(selectorEl).appendChild(optionEl); + } + function onChange(e) { + const value = selectorEl.selectedOptions[0].targetPropertyValue; + tr.b.Settings.set(settingsKey, value, opt_namespace); + targetEl[targetElProperty] = value; + } + const oldSetter = targetEl.__lookupSetter__('selectedIndex'); + selectorEl.__defineGetter__('selectedValue', function(v) { + return selectorEl.children[selectorEl.selectedIndex].targetPropertyValue; + }); + selectorEl.__defineGetter__('selectedItem', function(v) { + return selectorEl.children[selectorEl.selectedIndex].item; + }); + selectorEl.__defineSetter__('selectedValue', function(v) { + for (let i = 0; i < selectorEl.children.length; i++) { + const value = selectorEl.children[i].targetPropertyValue; + if (valuesEqual(value, v)) { + const changed = selectorEl.selectedIndex !== i; + if (changed) { + selectorEl.selectedIndex = i; + onChange(); + } + return; + } + } + throw new Error('Not a valid value'); + }); + + const initialValue = tr.b.Settings.get( + settingsKey, defaultValue, opt_namespace); + let didSet = false; + for (let i = 0; i < selectorEl.children.length; i++) { + if (valuesEqual(selectorEl.children[i].targetPropertyValue, + initialValue)) { + didSet = true; + targetEl[targetElProperty] = initialValue; + selectorEl.selectedIndex = i; + break; + } + } + if (!didSet) { + selectorEl.selectedIndex = defaultValueIndex; + targetEl[targetElProperty] = defaultValue; + } + + return selectorEl; + } + + function createEditCategorySpan(optionGroupEl, targetEl) { + const spanEl = createSpan({className: 'edit-categories'}); + Polymer.dom(spanEl).textContent = 'Edit categories'; + Polymer.dom(spanEl).classList.add('labeled-option'); + + spanEl.addEventListener('click', function() { + targetEl.onClickEditCategories(); + }); + return spanEl; + } + + function createOptionGroup(targetEl, targetElProperty, + settingsKey, defaultValue, + items) { + function onChange() { + let value = []; + if (this.value.length) { + value = this.value.split(','); + } + tr.b.Settings.set(settingsKey, value); + targetEl[targetElProperty] = value; + } + + const optionGroupEl = createSpan({className: 'labeled-option-group'}); + const initialValue = tr.b.Settings.get(settingsKey, defaultValue); + for (let i = 0; i < items.length; ++i) { + const item = items[i]; + const id = 'category-preset-' + item.label.replace(/ /g, '-'); + + const radioEl = document.createElement('input'); + radioEl.type = 'radio'; + Polymer.dom(radioEl).setAttribute('id', id); + Polymer.dom(radioEl).setAttribute('name', 'category-presets-group'); + Polymer.dom(radioEl).setAttribute('value', item.value); + radioEl.addEventListener('change', onChange.bind(radioEl, targetEl, + targetElProperty, + settingsKey)); + if (valuesEqual(initialValue, item.value)) { + radioEl.checked = true; + } + + const labelEl = document.createElement('label'); + Polymer.dom(labelEl).textContent = item.label; + Polymer.dom(labelEl).setAttribute('for', id); + + const spanEl = createSpan({className: 'labeled-option'}); + Polymer.dom(spanEl).appendChild(radioEl); + Polymer.dom(spanEl).appendChild(labelEl); + + spanEl.__defineSetter__('checked', function(opt_bool) { + const changed = radioEl.checked !== (!!opt_bool); + if (!changed) return; + + radioEl.checked = !!opt_bool; + onChange(); + }); + spanEl.__defineGetter__('checked', function() { + return radioEl.checked; + }); + + Polymer.dom(optionGroupEl).appendChild(spanEl); + } + Polymer.dom(optionGroupEl).appendChild( + createEditCategorySpan(optionGroupEl, targetEl)); + // Since this option group element is not yet added to the tree, + // querySelector will fail during updateEditCategoriesStatus_ call. + // Hence, creating the element with the 'expanded' classlist category + // added, if last selected value was 'Manual' selection. + if (!initialValue.length) { + Polymer.dom(optionGroupEl).classList.add('categories-expanded'); + } + targetEl[targetElProperty] = initialValue; + + return optionGroupEl; + } + + let nextCheckboxId = 1; + function createCheckBox(targetEl, targetElProperty, + settingsKey, defaultValue, + label, opt_changeCb) { + const buttonEl = document.createElement('input'); + buttonEl.type = 'checkbox'; + + let initialValue = defaultValue; + if (settingsKey !== undefined) { + initialValue = tr.b.Settings.get(settingsKey, defaultValue); + buttonEl.checked = !!initialValue; + } + if (targetEl) { + targetEl[targetElProperty] = initialValue; + } + + function onChange() { + if (settingsKey !== undefined) { + tr.b.Settings.set(settingsKey, buttonEl.checked); + } + if (targetEl) { + targetEl[targetElProperty] = buttonEl.checked; + } + if (opt_changeCb) { + opt_changeCb.call(); + } + } + + buttonEl.addEventListener('change', onChange); + + const id = '#checkbox-' + nextCheckboxId++; + + const spanEl = createSpan(); + spanEl.style.display = 'flex'; + spanEl.style.whiteSpace = 'nowrap'; + Polymer.dom(buttonEl).setAttribute('id', id); + + const labelEl = document.createElement('label'); + Polymer.dom(labelEl).textContent = label; + Polymer.dom(labelEl).setAttribute('for', id); + Polymer.dom(spanEl).appendChild(buttonEl); + Polymer.dom(spanEl).appendChild(labelEl); + + spanEl.__defineSetter__('checked', function(opt_bool) { + const changed = buttonEl.checked !== (!!opt_bool); + if (!changed) return; + + buttonEl.checked = !!opt_bool; + onChange(); + }); + spanEl.__defineGetter__('checked', function() { + return buttonEl.checked; + }); + + return spanEl; + } + + /** + * @param {!string} label + * @param {function()=} opt_callback + * @param {*=} opt_this + */ + function createButton(label, opt_callback, opt_this) { + const buttonEl = document.createElement('input'); + buttonEl.type = 'button'; + buttonEl.value = label; + + function onClick() { + opt_callback.call(opt_this || buttonEl); + } + + if (opt_callback) { + buttonEl.addEventListener('click', onClick); + } + + return buttonEl; + } + + function createTextInput( + targetEl, targetElProperty, settingsKey, defaultValue) { + const initialValue = tr.b.Settings.get(settingsKey, defaultValue); + const el = document.createElement('input'); + el.type = 'text'; + function onChange(e) { + tr.b.Settings.set(settingsKey, el.value); + targetEl[targetElProperty] = el.value; + } + el.addEventListener('input', onChange); + el.value = initialValue; + targetEl[targetElProperty] = initialValue; + + return el; + } + + function isElementAttachedToDocument(el) { + let cur = el; + while (Polymer.dom(cur).parentNode) { + cur = Polymer.dom(cur).parentNode; + } + return (cur === el.ownerDocument || cur.nodeName === '#document-fragment'); + } + + function asHTMLOrTextNode(value, opt_ownerDocument) { + if (value instanceof Node) { + return value; + } + const ownerDocument = opt_ownerDocument || document; + return ownerDocument.createTextNode(value); + } + + return { + createSpan, + createLink, + createDiv, + createScopedStyle, + createSelector, + createOptionGroup, + createCheckBox, + createButton, + createTextInput, + isElementAttachedToDocument, + asHTMLOrTextNode, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers_test.html new file mode 100644 index 00000000000..46313143710 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers_test.html @@ -0,0 +1,169 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const THIS_DOC = document.currentScript.ownerDocument; + + test('simpleSpanAndDiv', function() { + const divEl = tr.ui.b.createDiv({ + className: 'a-div-class', parent: document.body + }); + const testText = 'some span text'; + const spanEl = tr.ui.b.createSpan({ + className: 'a-span-class', + textContent: testText, + parent: divEl + }); + const eltInDocument = Polymer.dom(document) + .querySelector('.a-div-class>.a-span-class'); + assert.strictEqual(Polymer.dom(eltInDocument).textContent, testText); + Polymer.dom(eltInDocument.parentElement).removeChild(eltInDocument); + }); + + test('createSpan_ownerDocument', function() { + const spanEl = tr.ui.b.createSpan({ + className: 'a-span-class', + bold: true, + ownerDocument: THIS_DOC + }); + assert.strictEqual(spanEl.ownerDocument, THIS_DOC); + }); + + test('createLink', function() { + const linkEl = tr.ui.b.createLink({ + parent: document.body, + className: 'a-link-class', + textContent: 'Google', + href: 'http://www.google.com/' + }); + const eltInDocument = Polymer.dom(document) + .querySelector('.a-link-class'); + assert.strictEqual(Polymer.dom(eltInDocument).textContent, 'Google'); + assert.strictEqual(eltInDocument.href, 'http://www.google.com/'); + Polymer.dom(eltInDocument.parentElement).removeChild(eltInDocument); + }); + + test('checkboxFromDefaults', function() { + const target = {foo: undefined}; + const cb = tr.ui.b.createCheckBox( + target, 'foo', 'myCheckBox', false, 'Foo'); + assert.isFalse(target.foo); + }); + + test('checkboxFromSettings', function() { + tr.b.Settings.set('myCheckBox', true); + const target = {foo: undefined}; + const cb = tr.ui.b.createCheckBox( + target, 'foo', 'myCheckBox', false, 'Foo'); + assert.isTrue(target.foo); + }); + + test('checkboxChanged', function() { + const target = {foo: undefined}; + const cb = tr.ui.b.createCheckBox( + target, 'foo', 'myCheckBox', false, 'Foo'); + cb.checked = true; + + assert.isTrue(tr.b.Settings.get('myCheckBox', undefined)); + assert.isTrue(target.foo); + }); + + test('selectorSettingsAlreaySet', function() { + tr.b.Settings.set('myScale', 0.25); + + const target = { + scale: 314 + }; + const sel = tr.ui.b.createSelector( + target, 'scale', + 'myScale', 0.375, + [{label: '6.25%', value: 0.0625}, + {label: '12.5%', value: 0.125}, + {label: '25%', value: 0.25}, + {label: '37.5%', value: 0.375}, + {label: '50%', value: 0.5}, + {label: '75%', value: 0.75}, + {label: '100%', value: 1}, + {label: '200%', value: 2} + ]); + assert.strictEqual(target.scale, 0.25); + assert.strictEqual(sel.selectedIndex, 2); + }); + + test('selectorSettingsDefault', function() { + const target = { + scale: 314 + }; + const sel = tr.ui.b.createSelector( + target, 'scale', + 'myScale', 0.375, + [{label: '6.25%', value: 0.0625}, + {label: '12.5%', value: 0.125}, + {label: '25%', value: 0.25}, + {label: '37.5%', value: 0.375}, + {label: '50%', value: 0.5}, + {label: '75%', value: 0.75}, + {label: '100%', value: 1}, + {label: '200%', value: 2} + ]); + assert.strictEqual(target.scale, 0.375); + assert.strictEqual(sel.selectedIndex, 3); + }); + + test('selectorSettingsChanged', function() { + const target = { + scale: 314 + }; + const sel = tr.ui.b.createSelector( + target, 'scale', + 'myScale', 0.375, + [{label: '6.25%', value: 0.0625}, + {label: '12.5%', value: 0.125}, + {label: '25%', value: 0.25}, + {label: '37.5%', value: 0.375}, + {label: '50%', value: 0.5}, + {label: '75%', value: 0.75}, + {label: '100%', value: 1}, + {label: '200%', value: 2} + ]); + assert.strictEqual(sel.selectedValue, 0.375); + sel.selectedValue = 0.75; + assert.strictEqual(target.scale, 0.75); + assert.strictEqual(sel.selectedValue, 0.75); + assert.strictEqual(undefined), 0.75, tr.b.Settings.get('myScale'); + }); + + test('asHTMLOrTextNode_string', function() { + // Default owner document. + let node = tr.ui.b.asHTMLOrTextNode('Hello, World!'); + assert.instanceOf(node, Node); + assert.strictEqual(Polymer.dom(node).textContent, 'Hello, World!'); + assert.strictEqual(node.ownerDocument, document); + + // Custom owner document. + node = tr.ui.b.asHTMLOrTextNode('Bye, World!', THIS_DOC); + assert.instanceOf(node, Node); + assert.strictEqual(Polymer.dom(node).textContent, 'Bye, World!'); + assert.strictEqual(node.ownerDocument, THIS_DOC); + }); + + test('asHTMLOrTextNode_node', function() { + // Node object. Owner document should NOT be modified. + let node = document.createTextNode('Hi', THIS_DOC); + assert.strictEqual(tr.ui.b.asHTMLOrTextNode(node), node); + assert.strictEqual(node.ownerDocument, document); + + // HTMLElement object. Owner document should NOT be modified. + node = THIS_DOC.createElement('div'); + assert.strictEqual(tr.ui.b.asHTMLOrTextNode(node), node); + assert.strictEqual(node.ownerDocument, THIS_DOC); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle.html b/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle.html new file mode 100644 index 00000000000..614c3c2fb8f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle.html @@ -0,0 +1,185 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/ui.html"> + +<dom-module id="tr-ui-b-drag-handle"> + <template> + <style> + :host { + -webkit-user-select: none; + box-sizing: border-box; + display: block; + } + + :host(.horizontal-drag-handle) { + background-image: -webkit-gradient(linear, + 0 0, 0 100%, + from(#E5E5E5), + to(#D1D1D1)); + border-bottom: 1px solid #8e8e8e; + border-top: 1px solid white; + cursor: ns-resize; + flex: 0 0 auto; + height: 7px; + position: relative; + } + + :host(.vertical-drag-handle) { + background-image: -webkit-gradient(linear, + 0 0, 100% 0, + from(#E5E5E5), + to(#D1D1D1)); + border-left: 1px solid white; + border-right: 1px solid #8e8e8e; + cursor: ew-resize; + flex: 0 0 auto; + position: relative; + width: 7px; + } + </style> + <div></div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-b-drag-handle', + + created() { + this.lastMousePos_ = 0; + this.onMouseMove_ = this.onMouseMove_.bind(this); + this.onMouseUp_ = this.onMouseUp_.bind(this); + this.addEventListener('mousedown', this.onMouseDown_); + this.target_ = undefined; + this.horizontal = true; + this.observer_ = new WebKitMutationObserver( + this.didTargetMutate_.bind(this)); + this.targetSizesByModeKey_ = {}; + this.currentDraggingSize_ = undefined; + }, + + get modeKey_() { + return this.target_.className === '' ? '.' : this.target_.className; + }, + + get target() { + return this.target_; + }, + + set target(target) { + this.observer_.disconnect(); + this.target_ = target; + if (!this.target_) return; + this.observer_.observe(this.target_, { + attributes: true, + attributeFilter: ['class'] + }); + }, + + get horizontal() { + return this.horizontal_; + }, + + set horizontal(h) { + this.horizontal_ = h; + if (this.horizontal_) { + this.className = 'horizontal-drag-handle'; + } else { + this.className = 'vertical-drag-handle'; + } + }, + + get vertical() { + return !this.horizontal_; + }, + + set vertical(v) { + this.horizontal = !v; + }, + + forceMutationObserverFlush_() { + const records = this.observer_.takeRecords(); + if (records.length) { + this.didTargetMutate_(records); + } + }, + + didTargetMutate_(e) { + const modeSize = this.targetSizesByModeKey_[this.modeKey_]; + if (modeSize !== undefined) { + this.setTargetSize_(modeSize); + return; + } + + // If we hadn't previously sized the target, then just remove any manual + // sizing that we applied. + this.target_.style[this.targetStyleKey_] = ''; + }, + + get targetStyleKey_() { + return this.horizontal_ ? 'height' : 'width'; + }, + + getTargetSize_() { + // Get the actual size, which may be different from the expected size + // because of size constraints (e.g. min-width) etc. + const size = + parseInt(window.getComputedStyle(this.target_)[this.targetStyleKey_]); + this.targetSizesByModeKey_[this.modeKey_] = size; + return size; + }, + + setTargetSize_(s) { + this.target_.style[this.targetStyleKey_] = s + 'px'; + this.targetSizesByModeKey_[this.modeKey_] = this.getTargetSize_(); + tr.b.dispatchSimpleEvent(this, 'drag-handle-resize', true, false); + }, + + applyDelta_(delta) { + // Apply new size to the target. + if (this.target_ === this.nextElementSibling) { + this.currentDraggingSize_ += delta; + } else { + this.currentDraggingSize_ -= delta; + } + this.setTargetSize_(this.currentDraggingSize_); + }, + + onMouseMove_(e) { + // Compute the difference in height position. + const curMousePos = this.horizontal_ ? e.clientY : e.clientX; + const delta = this.lastMousePos_ - curMousePos; + + this.applyDelta_(delta); + + this.lastMousePos_ = curMousePos; + e.preventDefault(); + return true; + }, + + onMouseDown_(e) { + if (!this.target_) return; + this.forceMutationObserverFlush_(); + // Start with the current actual size. + this.currentDraggingSize_ = this.getTargetSize_(); + this.lastMousePos_ = this.horizontal_ ? e.clientY : e.clientX; + document.addEventListener('mousemove', this.onMouseMove_); + document.addEventListener('mouseup', this.onMouseUp_); + e.preventDefault(); + return true; + }, + + onMouseUp_(e) { + document.removeEventListener('mousemove', this.onMouseMove_); + document.removeEventListener('mouseup', this.onMouseUp_); + e.preventDefault(); + this.currentDraggingSize_ = undefined; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle_test.html new file mode 100644 index 00000000000..9faa91fce81 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle_test.html @@ -0,0 +1,128 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const createDragHandle = function() { + const el = document.createElement('div'); + el.style.border = '1px solid black'; + el.style.width = '200px'; + el.style.height = '200px'; + el.style.display = 'flex'; + el.style.flexDirection = 'column'; + + const upperEl = document.createElement('div'); + upperEl.style.flex = '1 1 auto'; + upperEl.style.minHeight = '0'; + + const lowerEl = document.createElement('div'); + lowerEl.style.height = '100px'; + lowerEl.style.minHeight = '50px'; + + const dragHandle = document.createElement('tr-ui-b-drag-handle'); + dragHandle.target = lowerEl; + + Polymer.dom(el).appendChild(upperEl); + Polymer.dom(el).appendChild(dragHandle); + Polymer.dom(el).appendChild(lowerEl); + el.upperEl = upperEl; + el.dragHandle = dragHandle; + el.lowerEl = lowerEl; + + el.getLowerElHeight = function() { + return parseInt(getComputedStyle(this.lowerEl).height); + }; + return el; + }; + + test('instantiate', function() { + this.addHTMLOutput(createDragHandle()); + }); + + test('dragWithoutConstraint', function() { + const el = createDragHandle(); + this.addHTMLOutput(el); + + const dragHandle = el.dragHandle; + assert.strictEqual(el.getLowerElHeight(), 100); + dragHandle.onMouseDown_({clientX: 0, clientY: 0, preventDefault() {}}); + dragHandle.onMouseMove_({clientX: 0, clientY: -10, preventDefault() {}}); + assert.strictEqual(el.getLowerElHeight(), 110); + dragHandle.onMouseUp_({preventDefault() {}}); + }); + + test('dragWithConstraint', function() { + const el = createDragHandle(); + this.addHTMLOutput(el); + + const dragHandle = el.dragHandle; + assert.strictEqual(el.getLowerElHeight(), 100); + dragHandle.onMouseDown_({clientX: 0, clientY: 0, preventDefault() {}}); + dragHandle.onMouseMove_({clientX: 0, clientY: 60, preventDefault() {}}); + // The actual size is constrained by minHeight. + assert.strictEqual(el.getLowerElHeight(), 50); + dragHandle.onMouseUp_({preventDefault() {}}); + + // Drag again. Should based on the actual size. + dragHandle.onMouseDown_({clientX: 0, clientY: 0, preventDefault() {}}); + dragHandle.onMouseMove_({clientX: 0, clientY: -10, preventDefault() {}}); + assert.strictEqual(el.getLowerElHeight(), 60); + dragHandle.onMouseUp_({preventDefault() {}}); + }); + + test('classNameMutation', function() { + const el = createDragHandle(); + + const styleEl = document.createElement('style'); + Polymer.dom(styleEl).textContent = + '.mode-a { height: 100px; } .mode-b { height: 50px; }'; + Polymer.dom(document.head).appendChild(styleEl); + + this.addHTMLOutput(el); + + try { + const dragHandle = el.dragHandle; + const mouseDown = {clientX: 0, clientY: 0, preventDefault() {}}; + const mouseMove = {clientX: 0, clientY: -10, preventDefault() {}}; + const mouseUp = {preventDefault() {}}; + + el.lowerEl.className = 'mode-a'; + assert.strictEqual(el.getLowerElHeight(), 100); + dragHandle.onMouseDown_(mouseDown); + dragHandle.onMouseMove_(mouseMove); + assert.strictEqual(el.getLowerElHeight(), 110); + dragHandle.onMouseUp_(mouseUp); + + // Change the class, which should restore the layout + // to the default sizing for mode-b + el.lowerEl.className = 'mode-b'; + dragHandle.forceMutationObserverFlush_(); + assert.strictEqual(el.getLowerElHeight(), 50); + + dragHandle.onMouseDown_(mouseDown); + dragHandle.onMouseMove_(mouseMove); + assert.strictEqual(el.getLowerElHeight(), 60); + dragHandle.onMouseUp_(mouseUp); + + // Restore the class-a, which should restore the layout + // to sizing when we were changed. + el.lowerEl.className = 'mode-a'; + dragHandle.forceMutationObserverFlush_(); + assert.strictEqual(el.getLowerElHeight(), 110); + + dragHandle.onMouseDown_(mouseDown); + dragHandle.onMouseMove_(mouseMove); + assert.strictEqual(el.getLowerElHeight(), 120); + dragHandle.onMouseUp_(mouseUp); + } finally { + Polymer.dom(document.head).removeChild(styleEl); + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/draw_helpers.html b/chromium/third_party/catapult/tracing/tracing/ui/base/draw_helpers.html new file mode 100644 index 00000000000..449f3576274 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/draw_helpers.html @@ -0,0 +1,415 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/elided_cache.html"> +<link rel="import" href="/tracing/ui/base/event_presenter.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides various helper methods for drawing to a provided + * canvas. + */ +tr.exportTo('tr.ui.b', function() { + const elidedTitleCache = new tr.ui.b.ElidedTitleCache(); + const ColorScheme = tr.b.ColorScheme; + const colorsAsStrings = ColorScheme.colorsAsStrings; + + const EventPresenter = tr.ui.b.EventPresenter; + const blackColorId = ColorScheme.getColorIdForReservedName('black'); + + /** + * This value is used to allow for consistent style UI elements. + * Thread time visualisation uses a smaller rectangle that has this height. + * @const + */ + const THIN_SLICE_HEIGHT = 4; + + /** + * This value is used to for performance considerations when drawing large + * zoomed out traces that feature cpu time in the slices. If the waiting + * width is less than the threshold, we only draw the rectangle as a solid. + * @const + */ + const SLICE_WAITING_WIDTH_DRAW_THRESHOLD = 3; + + /** + * If the slice has mostly been waiting to be scheduled on the cpu, the + * wall clock will be far greater than the cpu clock. Draw the slice + * only as an idle slice, if the active width is not thicker than the + * threshold. + * @const + */ + const SLICE_ACTIVE_WIDTH_DRAW_THRESHOLD = 1; + + /** + * Should we elide text on trace labels? + * Without eliding, text that is too wide isn't drawn at all. + * Disable if you feel this causes a performance problem. + * This is a default value that can be overridden in tracks for testing. + * @const + */ + const SHOULD_ELIDE_TEXT = true; + + /** + * Draw the define line into |ctx|. + * + * @param {Context} ctx The context to draw into. + * @param {float} x1 The start x position of the line. + * @param {float} y1 The start y position of the line. + * @param {float} x2 The end x position of the line. + * @param {float} y2 The end y position of the line. + */ + function drawLine(ctx, x1, y1, x2, y2) { + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + } + + /** + * Draw the defined triangle into |ctx|. + * + * @param {Context} ctx The context to draw into. + * @param {float} x1 The first corner x. + * @param {float} y1 The first corner y. + * @param {float} x2 The second corner x. + * @param {float} y2 The second corner y. + * @param {float} x3 The third corner x. + * @param {float} y3 The third corner y. + */ + function drawTriangle(ctx, x1, y1, x2, y2, x3, y3) { + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.lineTo(x3, y3); + ctx.closePath(); + } + + /** + * Draw an arrow into |ctx|. + * + * @param {Context} ctx The context to draw into. + * @param {float} x1 The shaft x. + * @param {float} y1 The shaft y. + * @param {float} x2 The head x. + * @param {float} y2 The head y. + * @param {float} arrowLength The length of the head. + * @param {float} arrowWidth The width of the head. + */ + function drawArrow(ctx, x1, y1, x2, y2, arrowLength, arrowWidth) { + const dx = x2 - x1; + const dy = y2 - y1; + const len = Math.sqrt(dx * dx + dy * dy); + const perc = (len - arrowLength) / len; + const bx = x1 + perc * dx; + const by = y1 + perc * dy; + const ux = dx / len; + const uy = dy / len; + const ax = uy * arrowWidth; + const ay = -ux * arrowWidth; + + ctx.beginPath(); + drawLine(ctx, x1, y1, x2, y2); + ctx.stroke(); + + drawTriangle(ctx, + bx + ax, by + ay, + x2, y2, + bx - ax, by - ay); + ctx.fill(); + } + + /** + * Draw the provided slices to the screen. + * + * Each of the elements in |slices| must provide the follow methods: + * * start + * * duration + * * colorId + * * selected + * + * @param {Context} ctx The canvas context. + * @param {TimelineDrawTransform} dt The draw transform. + * @param {float} viewLWorld The left most point of the world viewport. + * @param {float} viewRWorld The right most point of the world viewport. + * @param {float} viewHeight The height of the viewport. + * @param {Array} slices The slices to draw. + * @param {bool} async Whether the slices are drawn with async style. + */ + function drawSlices(ctx, dt, viewLWorld, viewRWorld, viewHeight, slices, + async) { + const pixelRatio = window.devicePixelRatio || 1; + const height = viewHeight * pixelRatio; + const viewL = dt.xWorldToView(viewLWorld); + const viewR = dt.xWorldToView(viewRWorld); + + let darkRectHeight = THIN_SLICE_HEIGHT * pixelRatio; + + // Not enough space for both colors, use light color only. + if (height < darkRectHeight) { + darkRectHeight = 0; + } + + const lightRectHeight = height - darkRectHeight; + + ctx.save(); + const rect = new tr.ui.b.FastRectRenderer( + ctx, viewL, viewR, 2, 2, colorsAsStrings); + rect.setYandH(0, height); + + const lowSlice = tr.b.findLowIndexInSortedArray( + slices, + function(slice) { return slice.start + slice.duration; }, + viewLWorld); + + let hadTopLevel = false; + + for (let i = lowSlice; i < slices.length; ++i) { + const slice = slices[i]; + const x = slice.start; + if (x > viewRWorld) break; + + const xView = dt.xWorldToView(x); + let wView = 1; + if (slice.duration > 0) { + const w = Math.max(slice.duration, 0.000001); + wView = Math.max(dt.xWorldVectorToView(w), 1); + } + + const colorId = EventPresenter.getSliceColorId(slice); + const alpha = EventPresenter.getSliceAlpha(slice, async); + const lightAlpha = alpha * 0.70; + + if (async && slice.isTopLevel) { + rect.setYandH(3, height - 3); + hadTopLevel = true; + } else { + rect.setYandH(0, height); + } + + // If cpuDuration is available, draw rectangles proportional to the + // amount of cpu time taken. + if (!slice.cpuDuration) { + // No cpuDuration available, draw using only one alpha. + rect.fillRect(xView, wView, colorId, alpha); + continue; + } + + let activeWidth = wView * (slice.cpuDuration / slice.duration); + let waitingWidth = wView - activeWidth; + + // Check if we have enough screen space to draw the whole slice, with + // both color tones. + // + // Truncate the activeWidth to 0 if it is less than 'threshold' pixels. + if (activeWidth < SLICE_ACTIVE_WIDTH_DRAW_THRESHOLD) { + activeWidth = 0; + waitingWidth = wView; + } + + // Truncate the waitingWidth to 0 if it is less than 'threshold' pixels. + if (waitingWidth < SLICE_WAITING_WIDTH_DRAW_THRESHOLD) { + activeWidth = wView; + waitingWidth = 0; + } + + // We now draw the two rectangles making up the event slice. + // NOTE: The if statements are necessary for performance considerations. + // We do not want to force draws, if the width of the rectangle is 0. + // + // First draw the solid color, representing the 'active' part. + if (activeWidth > 0) { + rect.fillRect(xView, activeWidth, colorId, alpha); + } + + // Next draw the two toned 'idle' part. + // NOTE: We subtract 1 from the left-hand edge and draw one extra pixel to + // prevent drawing artifacts. Without this, the two parts of the slice + // ('active' and 'idle') may appear split apart. + if (waitingWidth > 0) { + // First draw the light toned top part. + rect.setYandH(0, lightRectHeight); + rect.fillRect(xView + activeWidth - 1, + waitingWidth + 1, colorId, lightAlpha); + // Then the solid bottom half. + rect.setYandH(lightRectHeight, darkRectHeight); + rect.fillRect(xView + activeWidth - 1, + waitingWidth + 1, colorId, alpha); + // Reset for the next slice. + rect.setYandH(0, height); + } + } + rect.flush(); + + if (async && hadTopLevel) { + // Draw a top border over async slices in order to visually separate + // them from events above it. + // See https://github.com/google/trace-viewer/issues/725. + rect.setYandH(2, 1); + for (let i = lowSlice; i < slices.length; ++i) { + const slice = slices[i]; + const x = slice.start; + if (x > viewRWorld) break; + + if (!slice.isTopLevel) continue; + + const xView = dt.xWorldToView(x); + let wView = 1; + if (slice.duration > 0) { + const w = Math.max(slice.duration, 0.000001); + wView = Math.max(dt.xWorldVectorToView(w), 1); + } + + rect.fillRect(xView, wView, blackColorId, 0.7); + } + rect.flush(); + } + + ctx.restore(); + } + + /** + * Draw the provided instant slices as lines to the screen. + * + * Each of the elements in |slices| must provide the follow methods: + * * start + * * duration with value of 0. + * * colorId + * * selected + * + * @param {Context} ctx The canvas context. + * @param {TimelineDrawTransform} dt The draw transform. + * @param {float} viewLWorld The left most point of the world viewport. + * @param {float} viewRWorld The right most point of the world viewport. + * @param {float} viewHeight The height of the viewport. + * @param {Array} slices The slices to draw. + * @param {Numer} lineWidthInPixels The width of the lines. + */ + function drawInstantSlicesAsLines( + ctx, dt, viewLWorld, viewRWorld, viewHeight, slices, lineWidthInPixels) { + const pixelRatio = window.devicePixelRatio || 1; + const height = viewHeight * pixelRatio; + + ctx.save(); + ctx.lineWidth = lineWidthInPixels * pixelRatio; + + const lowSlice = tr.b.findLowIndexInSortedArray( + slices, + function(slice) { return slice.start; }, + viewLWorld); + + for (let i = lowSlice; i < slices.length; ++i) { + const slice = slices[i]; + const x = slice.start; + if (x > viewRWorld) break; + + ctx.strokeStyle = EventPresenter.getInstantSliceColor(slice); + + const xView = dt.xWorldToView(x); + + ctx.beginPath(); + ctx.moveTo(xView, 0); + ctx.lineTo(xView, height); + ctx.stroke(); + } + ctx.restore(); + } + + /** + * Draws the labels for the given slices. + * + * The |slices| array must contain objects with the following API: + * * start + * * duration + * * title + * * didNotFinish (optional) + * + * @param {Context} ctx The graphics context. + * @param {TimelineDrawTransform} dt The draw transform. + * @param {float} viewLWorld The left most point of the world viewport. + * @param {float} viewRWorld The right most point of the world viewport. + * @param {Array} slices The slices to label. + * @param {bool} async Whether the slice labels are drawn with async style. + * @param {float} fontSize The font size. + * @param {float} yOffset The font offset. + */ + function drawLabels(ctx, dt, viewLWorld, viewRWorld, slices, async, + fontSize, yOffset) { + const pixelRatio = window.devicePixelRatio || 1; + const pixWidth = dt.xViewVectorToWorld(1); + + ctx.save(); + + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + ctx.font = (fontSize * pixelRatio) + 'px sans-serif'; + + if (async) { + ctx.font = 'italic ' + ctx.font; + } + + const cY = yOffset * pixelRatio; + + const lowSlice = tr.b.findLowIndexInSortedArray( + slices, + function(slice) { return slice.start + slice.duration; }, + viewLWorld); + + // Don't render text until it is 20px wide + const quickDiscardThreshold = pixWidth * 20; + for (let i = lowSlice; i < slices.length; ++i) { + const slice = slices[i]; + if (slice.start > viewRWorld) break; + + if (slice.duration <= quickDiscardThreshold) continue; + + // Clip slice boundaries to viewport. + const xLeftClipped = Math.max(slice.start, viewLWorld); + const xRightClipped = Math.min(slice.start + slice.duration, viewRWorld); + const visibleWidth = xRightClipped - xLeftClipped; + + const title = slice.title + + (slice.didNotFinish ? ' (Did Not Finish)' : ''); + + let drawnTitle = title; + let drawnWidth = elidedTitleCache.labelWidth(ctx, drawnTitle); + const fullLabelWidth = elidedTitleCache.labelWidthWorld( + ctx, drawnTitle, pixWidth); + if (SHOULD_ELIDE_TEXT && fullLabelWidth > visibleWidth) { + const elidedValues = elidedTitleCache.get( + ctx, pixWidth, + drawnTitle, drawnWidth, + visibleWidth); + drawnTitle = elidedValues.string; + drawnWidth = elidedValues.width; + } + + if (drawnWidth * pixWidth < visibleWidth) { + ctx.fillStyle = EventPresenter.getTextColor(slice); + const cX = dt.xWorldToView((xLeftClipped + xRightClipped) / 2); + ctx.fillText(drawnTitle, cX, cY, drawnWidth); + } + } + ctx.restore(); + } + + return { + drawSlices, + drawInstantSlicesAsLines, + drawLabels, + + drawLine, + drawTriangle, + drawArrow, + + elidedTitleCache_: elidedTitleCache, + + THIN_SLICE_HEIGHT, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown.html b/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown.html new file mode 100644 index 00000000000..0c37e9910b7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown.html @@ -0,0 +1,103 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +<dom-module id='tr-ui-b-dropdown'> + <template> + <style> + button { + @apply --dropdown-button; + } + button.open { + @apply --dropdown-button-open; + } + dialog { + position: absolute; + margin: 0; + padding: 1em; + border: 1px solid darkgrey; + @apply --dropdown-dialog; + } + </style> + + <button id="button" on-tap="open">[[label]]</button> + + <dialog id="dialog" on-tap="onDialogTap_" on-cancel="close"> + <slot></slot> + </dialog> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.ui.b', function() { + Polymer({ + is: 'tr-ui-b-dropdown', + + properties: { + label: { + type: String, + value: '', + }, + }, + + open() { + if (this.isOpen) return; + + Polymer.dom(this.$.button).classList.add('open'); + const buttonRect = this.$.button.getBoundingClientRect(); + this.$.dialog.style.top = buttonRect.bottom - 1 + 'px'; + this.$.dialog.style.left = buttonRect.left + 'px'; + this.$.dialog.showModal(); + + const dialogRect = this.$.dialog.getBoundingClientRect(); + if (dialogRect.right > window.innerWidth) { + // If the dialog's right edge falls past the right edge of the window, + // then move the dialog to the left so that its right edge lines up with + // the button's right edge, but not so far left that its left edge falls + // past the left edge of the window. + this.$.dialog.style.left = Math.max(0, buttonRect.right - + dialogRect.width) + 'px'; + } + }, + + onDialogTap_(event) { + // Clicking on elements inside the dialog should never close it. + if (event.detail.sourceEvent.srcElement !== this.$.dialog) return; + + // Close the dialog when the user clicks on the backdrop outside the + // dialog, which sends click events to the dialog even though the + // coordinates are outside the dialog. + const dialogRect = this.$.dialog.getBoundingClientRect(); + let inside = true; + inside &= event.detail.x >= dialogRect.left; + inside &= event.detail.x < dialogRect.right; + inside &= event.detail.y >= dialogRect.top; + inside &= event.detail.y < dialogRect.bottom; + if (inside) return; + + event.preventDefault(); + this.close(); + }, + + close() { + if (!this.isOpen) return; + this.$.dialog.close(); + Polymer.dom(this.$.button).classList.remove('open'); + this.$.button.focus(); + }, + + get isOpen() { + return this.$.button.classList.contains('open'); + } + }); + + return { + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown_test.html new file mode 100644 index 00000000000..bdc118a4d09 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown_test.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/dropdown.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function dispatchClick(elem, x, y) { + const clickEvent = document.createEvent('MouseEvents'); + const bubbles = true; + const cancelable = false; + const button = 0; + const ctrlKey = false; + const altKey = false; + const shiftKey = false; + const metaKey = false; + clickEvent.initMouseEvent( + 'click', bubbles, cancelable, document.defaultView, button, x, y, x, + y, ctrlKey, altKey, shiftKey, metaKey, button, elem); + elem.dispatchEvent(clickEvent); + } + + test('basic', function() { + const dd = document.createElement('tr-ui-b-dropdown'); + dd.style.marginLeft = '50px'; + dd.style.width = '50px'; + dd.label = 'Settings'; + + const textDiv = tr.ui.b.createDiv({textContent: 'text'}); + Polymer.dom(dd).appendChild(textDiv); + const target = {}; + const checkbox = tr.ui.b.createCheckBox( + target, 'enabled', undefined, true, 'checkbox'); + const actualCheckbox = checkbox.querySelector('input'); + Polymer.dom(dd).appendChild(checkbox); + + const container = tr.ui.b.createDiv(); + container.style.height = '100px'; + Polymer.dom(container).appendChild(dd); + Polymer.dom(container).appendChild( + tr.ui.b.createDiv({textContent: 'some text'})); + this.addHTMLOutput(container); + + dd.open(); + assert.isTrue(dd.isOpen); + + dd.close(); + assert.isFalse(dd.isOpen); + + dd.open(); + assert.isTrue(dd.isOpen); + + // Dispatching a click event at contents of the dropdown should never close + // it, even if it is outside of the dropdown, which can happen if the user + // presses the spacebar while the checkbox is focused. + const actualCheckboxRect = actualCheckbox.getBoundingClientRect(); + dispatchClick( + actualCheckbox, actualCheckboxRect.left, actualCheckboxRect.top); + assert.isTrue(dd.isOpen); + dispatchClick(actualCheckbox, 0, 0); + assert.isTrue(dd.isOpen); + + const textDivRect = textDiv.getBoundingClientRect(); + dispatchClick(textDiv, textDivRect.left, textDivRect.top); + assert.isTrue(dd.isOpen); + dispatchClick(textDiv, 0, 0); + assert.isTrue(dd.isOpen); + + // Clicking outside the dropdown should close it. + dispatchClick(dd.$.dialog, 0, 0); + assert.isFalse(dd.isOpen); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/elided_cache.html b/chromium/third_party/catapult/tracing/tracing/ui/base/elided_cache.html new file mode 100644 index 00000000000..85e6cf681f6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/elided_cache.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> +<script> +'use strict'; + +/** + * @fileoverview Provides a caching layer for elided text values. + */ +tr.exportTo('tr.ui.b', function() { + /** + * Cache for elided strings. + * Moved from the ElidedTitleCache protoype to a "global" for speed + * (variable reference is 100x faster). + * key: String we wish to elide. + * value: Another dict whose key is width + * and value is an ElidedStringWidthPair. + */ + const elidedTitleCacheDict = new Map(); + const elidedTitleCache = new ElidedTitleCache(); + + /** + * A cache for elided strings. + * @constructor + */ + function ElidedTitleCache() { + // TODO(jrg): possibly obsoleted with the elided string cache. + // Consider removing. + this.textWidthMap = new Map(); + } + + ElidedTitleCache.prototype = { + /** + * Return elided text. + * + * @param {ctx} Context The graphics context. + * @param {pixWidth} Pixel width. + * @param {title} Original title text. + * @param {width} Drawn width in world coords. + * @param {sliceDuration} Where the title must fit (in world coords). + * @return {ElidedStringWidthPair} Elided string and width. + */ + get(ctx, pixWidth, title, width, sliceDuration) { + let elidedDict = elidedTitleCacheDict.get(title); + if (!elidedDict) { + elidedDict = new Map(); + elidedTitleCacheDict.set(title, elidedDict); + } + + let elidedDictForPixWidth = elidedDict.get(pixWidth); + if (!elidedDictForPixWidth) { + elidedDict.set(pixWidth, new Map()); + elidedDictForPixWidth = elidedDict.get(pixWidth); + } + + let stringWidthPair = elidedDictForPixWidth.get(sliceDuration); + if (stringWidthPair === undefined) { + let newtitle = title; + let elided = false; + while (this.labelWidthWorld(ctx, newtitle, pixWidth) > sliceDuration) { + if (newtitle.length * 0.75 < 1) break; + newtitle = newtitle.substring(0, newtitle.length * 0.75); + elided = true; + } + + if (elided && newtitle.length > 3) { + newtitle = newtitle.substring(0, newtitle.length - 3) + '...'; + } + + stringWidthPair = new ElidedStringWidthPair( + newtitle, this.labelWidth(ctx, newtitle)); + elidedDictForPixWidth.set(sliceDuration, stringWidthPair); + } + return stringWidthPair; + }, + + quickMeasureText_(ctx, text) { + let w = this.textWidthMap.get(text); + if (!w) { + w = ctx.measureText(text).width; + this.textWidthMap.set(text, w); + } + return w; + }, + + labelWidth(ctx, title) { + return this.quickMeasureText_(ctx, title) + 2; + }, + + labelWidthWorld(ctx, title, pixWidth) { + return this.labelWidth(ctx, title) * pixWidth; + } + }; + + /** + * A pair representing an elided string and world-coordinate width + * to draw it. + * @constructor + */ + function ElidedStringWidthPair(string, width) { + this.string = string; + this.width = width; + } + + return { + ElidedTitleCache, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter.html b/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter.html new file mode 100644 index 00000000000..977561e2787 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2012 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/model/selection_state.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides color scheme related functions. + */ +tr.exportTo('tr.ui.b', function() { + const ColorScheme = tr.b.ColorScheme; + + const colors = ColorScheme.colors; + const colorsAsStrings = ColorScheme.colorsAsStrings; + + const SelectionState = tr.model.SelectionState; + + /** + * Provides methods to get view values for events. + */ + const EventPresenter = { + getSelectableItemColorAsString(item) { + const offset = this.getColorIdOffset_(item); + const colorId = ColorScheme.getVariantColorId(item.colorId, offset); + return colorsAsStrings[colorId]; + }, + + getColorIdOffset_(event) { + return event.selectionState; + }, + + getTextColor(event) { + if (event.selectionState === SelectionState.DIMMED) { + return 'rgb(60,60,60)'; + } + return 'rgb(0,0,0)'; + }, + + getSliceColorId(slice) { + const offset = this.getColorIdOffset_(slice); + return ColorScheme.getVariantColorId(slice.colorId, offset); + }, + + getSliceAlpha(slice, async) { + let alpha = 1; + if (async) { + alpha *= 0.3; + } + return alpha; + }, + + getInstantSliceColor(instant) { + const offset = this.getColorIdOffset_(instant); + const colorId = ColorScheme.getVariantColorId(instant.colorId, offset); + return colors[colorId].toStringWithAlphaOverride(1.0); + }, + + getObjectInstanceColor(instance) { + const offset = this.getColorIdOffset_(instance); + const colorId = ColorScheme.getVariantColorId(instance.colorId, offset); + return colors[colorId].toStringWithAlphaOverride(0.25); + }, + + getObjectSnapshotColor(snapshot) { + const offset = this.getColorIdOffset_(snapshot); + let colorId = snapshot.objectInstance.colorId; + colorId = ColorScheme.getVariantColorId(colorId, offset); + return colors[colorId]; + }, + + getCounterSeriesColor(colorId, selectionState, + opt_alphaMultiplier) { + const event = {selectionState}; + const offset = this.getColorIdOffset_(event); + const c = colors[ColorScheme.getVariantColorId(colorId, offset)]; + return c.toStringWithAlphaOverride( + opt_alphaMultiplier !== undefined ? opt_alphaMultiplier : 1.0); + }, + + getBarSnapshotColor(snapshot, offset) { + const snapshotOffset = this.getColorIdOffset_(snapshot); + let colorId = snapshot.objectInstance.colorId; + colorId = ColorScheme.getAnotherColorId(colorId, offset); + colorId = ColorScheme.getVariantColorId(colorId, snapshotOffset); + return colors[colorId].toStringWithAlphaOverride(1.0); + } + }; + + return { + EventPresenter, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter_test.html new file mode 100644 index 00000000000..f9531aa9cdf --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter_test.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/base/event_presenter.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const EventPresenter = tr.ui.b.EventPresenter; + + function mockEvent(colorId, selectionState) { + return { colorId, selectionState }; + } + + function mockSnapshot(colorId, selectionState) { + return { objectInstance: { colorId }, selectionState }; + } + + function isColor(color) { + return color.toString().startsWith('rgb'); + } + + test('instantSliceColor', function() { + const color = EventPresenter.getInstantSliceColor(mockEvent(1, 0)); + const variant = EventPresenter.getInstantSliceColor(mockEvent(1, 1)); + assert.isTrue(isColor(color)); + assert.notStrictEqual(color, variant); + }); + + test('objectInstanceColor', function() { + const color = EventPresenter.getObjectInstanceColor(mockEvent(2, 0)); + const variant = EventPresenter.getInstantSliceColor(mockEvent(2, 2)); + assert.isTrue(isColor(color)); + assert.notStrictEqual(color, variant); + }); + + test('objectSnapshotColor', function() { + const color = EventPresenter.getObjectSnapshotColor(mockSnapshot(3, 0)); + const variant = EventPresenter.getObjectSnapshotColor(mockSnapshot(3, 3)); + assert.isTrue(isColor(color)); + assert.notStrictEqual(color, variant); + }); + + test('counterSeriesColor', function() { + const color = EventPresenter.getCounterSeriesColor(1, 0); + const variant = EventPresenter.getCounterSeriesColor(1, 1); + const transparent = EventPresenter.getCounterSeriesColor(1, 0, 0.0); + assert.isTrue(isColor(color)); + assert.isTrue(isColor(transparent)); + assert.notStrictEqual(color, variant); + assert.notStrictEqual(variant, transparent); + assert.notStrictEqual(transparent, color); + }); + + test('barSnapshotColor', function() { + const color = EventPresenter.getBarSnapshotColor(mockSnapshot(1, 2), 1000); + assert.isTrue(isColor(color)); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/fast_rect_renderer.html b/chromium/third_party/catapult/tracing/tracing/ui/base/fast_rect_renderer.html new file mode 100644 index 00000000000..750140bc8f9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/fast_rect_renderer.html @@ -0,0 +1,147 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2012 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> +<script> +'use strict'; + +/** + * @fileoverview Provides a mechanism for drawing massive numbers of + * colored rectangles into a canvas in an efficient manner, provided + * they are drawn left to right with fixed y and height throughout. + * + * The basic idea used here is to fuse subpixel rectangles together so that + * we never issue a canvas fillRect for them. It turns out Javascript can + * do this quite efficiently, compared to asking Canvas2D to do the same. + * + * Rather than expending compute cycles trying to figure out an average + * color for fused rectangles from css strings, you instead draw using + * palettized colors. The fused rect color is chosen from the rectangle with + * the higher alpha value, if equal the max palette index encountered. + * + * Make sure to flush the trackRenderer before finishing drawing in order + * to commit any queued drawing operations. + */ +tr.exportTo('tr.ui.b', function() { + /** + * Creates a fast rect renderer with a specific set of culling rules + * and color palette. + * + * Rectangles that are drawn will be clipped horizontally to the range + * [xMin, xMax]; this is done because CanvasRenderingContext2D does not draw + * rectangles with coordinates of very large magnitude correctly. + * + * @param {GraphicsContext2D} ctx Canvas2D drawing context. + * @param {number} xMin Left border of the viewport (pre-transformation). + * @param {number} xMax Right border of the viewport (pre-transformation). + * @param {number} minRectSize Only rectangles with width < minRectSize are + * considered for merging. + * @param {number} maxMergeDist Only rectangles that are at most this far + apart are considered for merging. + * @param {Array} palette The color palette for drawing. Palette slots + * should map to valid Canvas fillStyle strings. + * + * @constructor + */ + function FastRectRenderer( + ctx, xMin, xMax, minRectSize, maxMergeDist, palette) { + this.ctx_ = ctx; + this.xMin_ = xMin; + this.xMax_ = xMax; + this.minRectSize_ = minRectSize; + this.maxMergeDist_ = maxMergeDist; + this.palette_ = palette; + } + + FastRectRenderer.prototype = { + y_: 0, + h_: 0, + merging_: false, + mergeStartX_: 0, + mergeCurRight_: 0, + mergedColorId_: 0, + mergedAlpha_: 0, + + /** + * Changes the y position and height for subsequent fillRect + * calls. x and width are specified on the fillRect calls. + */ + setYandH(y, h) { + if (this.y_ === y && + this.h_ === h) { + return; + } + this.flush(); + this.y_ = y; + this.h_ = h; + }, + + /** + * Fills rectangle at the specified location, if visible. If the + * rectangle is subpixel, it will be merged with adjacent rectangles. + * The drawing operation may not take effect until flush is called. + * @param {number} colorId The color of this rectangle, as an index + * in the renderer's color palette. + * @param {number} alpha The opacity of the rectangle as 0.0-1.0 number. + */ + fillRect(x, w, colorId, alpha) { + const r = x + w; + if (w < this.minRectSize_) { + if (r - this.mergeStartX_ > this.maxMergeDist_) { + this.flush(); + } + if (!this.merging_) { + this.merging_ = true; + this.mergeStartX_ = x; + this.mergeCurRight_ = r; + this.mergedColorId_ = colorId; + this.mergedAlpha_ = alpha; + } else { + this.mergeCurRight_ = r; + + if (this.mergedAlpha_ < alpha || + (this.mergedAlpha_ === alpha && this.mergedColorId_ < colorId)) { + this.mergedAlpha_ = alpha; + this.mergedColorId_ = colorId; + } + } + } else { + if (this.merging_) { + this.flush(); + } + this.ctx_.fillStyle = this.palette_[colorId]; + this.ctx_.globalAlpha = alpha; + const xLeft = Math.max(x, this.xMin_); + const xRight = Math.min(r, this.xMax_); + if (xLeft < xRight) { + this.ctx_.fillRect(xLeft, this.y_, xRight - xLeft, this.h_); + } + } + }, + + /** + * Commits any pending fillRect operations to the underlying graphics + * context. + */ + flush() { + if (this.merging_) { + this.ctx_.fillStyle = this.palette_[this.mergedColorId_]; + this.ctx_.globalAlpha = this.mergedAlpha_; + const xLeft = Math.max(this.mergeStartX_, this.xMin_); + const xRight = Math.min(this.mergeCurRight_, this.xMax_); + if (xLeft < xRight) { + this.ctx_.fillRect(xLeft, this.y_, xRight - xLeft, this.h_); + } + this.merging_ = false; + } + } + }; + + return { + FastRectRenderer, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/favicons.html b/chromium/third_party/catapult/tracing/tracing/ui/base/favicons.html new file mode 100644 index 00000000000..0182602631d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/favicons.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2012 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +<script> +'use strict'; +tr.exportTo('tr.ui.b', function() { + const FaviconsByHue = { + blue: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAAC4CAYAAABQMybHAAAlrklEQVR4Ae2dCXwdVb3H5265yc3SpEk3ukEXCqVUBLT4Wm19oFKtaN0fKijy9CMguPBarIJsIiA8qsjTh7SllAoFeVBaEARkLV1ooXtL0yRdkqZp9u3uy/v/5uY/OZm75y659+acdnLOnP385zv/+58zZ2YMinTplIAhzsoDceaT2RKUQLwHIMFqh0V2ll0kn4XA6byv9/Vw834kX19e7keRQCzhRyk6bJJYRvD1YTXuhRdeqDj77LPPtNls400mU7HRaCzFFggEVJ/iSqhsicFgKIXUKL6bvB6fz9fj9/u7Kb4bPjaK67Xb7Q0HDhw49IUvfKEd2XUb7WpxHIYvXRgJ8AELkzRso1gmKrwkBfjG7373u5Zly5ZNKS8vn2G1Ws80m83YphPI0wnQUemQFp0IzQR9tdfrxXbI5XId6ujo+PCuu+6qXbNmjYfa9NMmngDoBmt+hIe944M53AUhwqwCvXTp0qJrr732opKSkk8XFhZ+imC+gIAryAZB0QnlJuB3OJ3Ot3p6el5/6KGHttxzzz0O6pse+GEP+3AGnKE2EhgG0tAFt99++4WkoT9tsVgW0DaH4guzAeg4+uD0eDxbaXuDNPzrt9xyy3bS8G4qB8BF6OOoKr+yDDfAB0B91VVXFf72t7+9lLT05QUFBZfQoYWtnA+ux+12v0ra/W+/+tWvXlq5cqWTBjUsYR8OgDPU8KGtjR9++OHHx4wZ8+2ioqKv0X4lbfnsWh0Ox9+bmprWzpgxYxsNFBpd1Op5bcbkM+AMtgr11q1bTz/zzDP/gy4Qv02zGtPzmehIY6MZmmq6UF176NChJ+bMmXOkD3QR9khFczY+HwEXwTbV1NTMI229FCYIXSTm43gTho8uUgMwYUir3zN16tR3qAIfbXkJej4dcIxF1dbkm44ePfqZqqqqpTT7MZf2pYsgAZqN2dTS0nLP5MmTX6EsDDrDHqFU7kTnA+Aa2BMmTDBv2bLliyNHjlxCZsgFuXMYhr6nZL7saGtru/eiiy7aUF9f76UeAfKcBz2XAUffVbgJbAuB/Y3KysoldONl5tDjkrs9oBtL+1tbWwH6UwS6/mZSzg0sVwHXTJG9e/deOGXKlOWksS/MOelncYdJo2+vra396axZs7ZTN0XTJYt7Hdq1XANc1dg0DNOqVatGLl68+DZa/3E1XTwCeOn6JLCly6ncU9+mNLnBZRLOYPAHHI5H2l5/8TdHbl3SRjUx6DkztZgrgKOfDLf5xIkT36moqLiLzJG0rAFJAomsKDp1W51S74IZnSIX8DcrXV3LlK/Oe5xqZPsckGc96LkAOPpowrZ79+5ZNK31BzkzQtKI4qxvV0dJTSLJ592kHKu7QfnPxXupFmhzbFkNeTb/tGsae/bs2Va6wr/lrLPO2izhTgLQZIuaaMp1yvTNyvNbb1HomFB1ZtrAUNYqymztGMNt2rhx44T58+evohs1n0r2+AyX8mnT4KIAvZ63lA82f1/55TX1FJ21tnk2As4zJObq6urP0BTgCmlri2TFDmcEcHQDtnlz4w+Uyz+Hm0Rsm2PuPGtcNpkomtZesGBBYXNz8210d+05CXfWsBLaEQNd5I+e8JyyYettCh0zyoBrpawyWbJFg2twv/jiixPnzZu3mhZFzQ2VqIyJRwIZ0+BiZzyeTcqebVcqS350nKKzxmTJBsDRB3WWZN++fXPpps060tpVouxkODEJDAng6GIg0KI0Hv+mcsXnN9FeVsyyDLWJwnCbadXfomnTpm2UcCcGc1blNhiqlNMmblT+9soi6hdmWKC4hlSJDiXgaBsCsNDKvysnTpz4JIWLaJMupyVgKFLGjHtSefrNK2kYFtpwjIeMs6FqWIOb7kr+Yty4cX+m2+0446XLBwkESHuPrPqz8uymX9BwhhTyoQBchZseQiigdcj30grAO+SDCPlAtW4MeLikdMQdyvqt9yp0rCl1SDR5pgFX4V64cGERvdhmRWlp6XU6scjdfJNAcfF1ysqNK5Q5C2F+ZhzyTF4AqHCPGjXKSjdwHqUHfr+ab8cyW8YzZLMo0QTgcj2jfO/S7ynNzS7KxtOI0UqkJC1TGlyFm3pccPDgwfsk3Ck5drlVidX6VWXFxvvAAG0Z0+SZAJzhtjQ2Ni6ld5D8KLeOjOxtyiRgK/6R8uy7S6m+jF14phtwmEBow3L8+PGr6FnJm1MmLFlRbkqgtOxm5am3rgITtIGNtJrJ6QQcHcdPkYUuKL9MsybLKSydlICijKxcrjz+0pdJFKzJ0wZ5ugBnuM27du2aT7ffV9JUIGCXTkqAJEAsjJ2wQlm1fj7tpPWOZzoAB9yo1/zSSy/NoLdJraMwFsdLJyUgSqBQGX/GOuX+FTMoEpCDmZRr8nQBbqIHgovnzp27mtaWlImjkmEpAU0CYGPmR1crF19cTHH4hU854KmuECcMOmo9derUAyNGjLiawtJlWAJZOQ8eTQb27keUyz7xM8qS8jnyVGpwNk0s+/fv/4qEO9oRlWkDJGArvVpZ89JXKC7lMyupApzhNm/YsGH6GWec8eCAAcgdKYFYEhhz2oPK3X+ZTtlSao+nEnDzxWRL0eNmj0q7O9bRlOkhEoA9ft6cR5WPq/Y4IE+J+ZyKSjS7m56jvK+srEzeqQw5epmNyDkbXBRPT8//Kl++6EaKSok9nqwG10yTHTt2fJpWB0q4xYMlw4lLoJhu5z/y3KepYEpMlWQBV7U3mSXFNN99H71YPfEByRJSAqIEwND4yfcpFyzgqcOkGE2mMGtvy2OPPXY9vZjnTLGfMiwlMGgJWCxnKktv/QmVT3pWZbCAM9zmxx9//IzRo0fj0STppARSJ4HykTcqN//3GVRhUqZKMoCrC6no6Zy7yTSxpW5ksiYpAZKA0WhTPj73dxRKakHWYABn7W3Zs2cPvjH5eXlApATSIoGi4i8oK56/tA9ysAr2EnKDARxlzJdddlkJ3dC5N6HWZGYpgUQlMH7SvbRWpYSKsamSUA2JAs7a2/ynP/3pOvrc9eSEWpOZpQQSlYDZPFn54a/xcDoDnpAWTxRw5DfRJ7DL6HUPP060rzK/lMCgJFA+8sfKZd/CqlRc9yXEbCKZWXtbli1b9gN6EX3loDorC0kJJCoBk6lS+ebVP6BiCU8bJgI48ppxU2fs2LHXJNpHmV9KICkJVFZdo3zsY7j5w6ZKXNXFCzhrb/PDDz/8HbK9x8ZVu8wkJZAqCZjNY5Wf3vkdqo4Bj8sWjxdw5DPRt3KKTjvtNNxhkk5KIPMSqBz1E2Xq7ITekBUP4Ky9LevWrfsGae9JmR+ZbFFKgCRgLpik3HL3NygUty0eD+Cq9h4/fnwBbTdIQUsJDKkERo+9QSkr47ubMfmNlQHaG5v56aef/ndaUDVtSAcnG5cSMFumKXc/fDGYpI35jCiXeADH3KOZ7lp+Sy6HjShHmZApCWA57dgJ3wKTtIFNQB7RxQIc6abLL7+cniEesTBiLTJBSiCTEiguWah8/isjqEkAHpXhaIk4M5BuXrp06ZfoOUtcvUonJTD0EjCaipSvff9L1JGYU4bRAEeaCjh9P+fr0jwZ+uMqe9AnAZgpo0Z/nfYY8IgcR0qA9sZmeuCBBybZbLZ/66taelIC2SEBKzF5zTJMWbMdDl5DXDTAVe29aNGib5D2jpQvpEIZISWQEQkYicm5C0QtnjDg6uwJPY72tYx0WDYiJZCoBMorGXDW4iE1hNPMOBMQb1qzZs0MmvueHlJKRmS1BCZYYZoOA2exTFd+dT/eTsuzKSFaPJwkNMDPO++8+fLiMvdA+Z8JJcqPN+9RGnocoZ0PBELjFF2cbjdYIEykvq4wWehd4APb05dBari4gaWCe/p8AT+uFOdT4j7aoJTB7oAGowFurqqqmicBV5QPmgLKX3b7lVbHANmRLLPVVSjnGT6hzFRa44dHHEqIHhQThXC8+YQiqQ66K9rnvakoD1O9DPiAJvSAo8vYjMXFxWZ6U9VFA3IP052fv+5VGntzBW4+SCYl4KtQ/L3tpCBJ0+WpC/hKLgKrvb29DDj41Q4WIvUOcaZHH310lslkGqlPHI77uQd38CgZTBbSVBVKXk+CGYwjS758/ywwS1sIz/oI1uCmmTNnflKaJ7l/OmuQG3migQ9xnvg0W2gaN/2TfYDzoLQDFw5wVYOT/T1XAq7JKacDKuS2csVg1B/unB6W2nkwaiiumEs7rMEBueZEG5zpN9Gt+QKyv+douWQg5yXAkPvtHYO78MxiCZisJXNsVRML7C3HndRN5li1w/WnNPaNDz744Ll0ZpRm8Zhk1wYhAYacjPJBlM7eIgHFUFryxZvPpR6q/Io9DavBJ0yYcJY0T0Qx5U84CDnNrtjb82dQZHqZysefRQPaRltEDc4JRlr7PS1/Ri9HopeAwWRWjLYKQiF/NLnBWgpmocGZY3XYoomCBOybaPXgNKnBVfnk7R8V8qLyvIAcrBoLiqaCXdoYcvXYMeB8KmPfSIBPUVPln7yWQD/kjEEOD7fABsBVfvtGoTIdYoOPHDnSXFhYODmHhyq7noAEgpCPUPyOTiql3QBMoIbsyGo0F04uInYdbW3RTZRbb711AnXZmh3dlr3IhAQYcpooz0RzaWmDTk1r0YLrwS4GwRaJuoMGmXrjOeecI5fHQiLDzKmQF9ILXFXIGYfc8q2jZ4JdBlyFnE9ZHolx1KhR8gJzmMHNw9Ugz8U7nrijWToyZCZFtMEBu7GoqGgiD1j6w08CKuTWUsXv6s65O56GApVdlWM+cnoNbqB3D+JzEdINYwkw5DlnkxvNYJetEdVEETU4Ioy0RLZEzoEPY7r7hh6EvIQ0eQ/FZP/sCpilPgNwKG0VbgyFdzTqCXC8ZFw6KQEAoxgLS3NoPbkR7GosIyxqcBxSgwQcYpCOJWDAOnIrKUbS5AH9M5GcKUt8OiEZcK1HbIMjQiVfAq7JRgb6JADIDQR5tpuuAaMGuGaisAbXIiTgkutwEujX5L2UnJ02uSEIOHdfZVpqcBaH9GNKIKjJQ6yAmOUylYHsa+6cprBZg3MfpA3OkpB+WAkENXmxEnDbs2+e3KABrvU9RINTih56LbMMSAlAAqomL7BRQFOU2SGYgMouOqV1jGHWIrxer50+8iofV8uOQ5a1vVA1OUEecOPtWdlhkxsUH/2saE5lmufBtVifz4erCOmkBGJKIKjJ8V0ETT/GLJPODAG/X8+uOg+O0087BaHB09kJWXd+SSCoyYuUgIceaB/qeXL/AA2uci3a4JB8QGrw/AIwE6NRNbmlcMht8oBftT40ZY2xsw2OsJogAYcopEtUAqomt5Am9w6dJg8ENPNagzysBs/2W7KJCl/mz4wE8OYsg3loNLnKbNAG1+DGqFmDI1LdpA2eGRjytRX19XAEecDr6kMqcyM1BNTrR41ltCxqcAYc6yOlkxIYtASCmhyP9WZ2doVmUXhtL1hWHWtw3lccDkcb1H22L6zROiwDWSmBoCa39mnyDHSRmPV7nG36lliDs1r3t7e31+kzyX0pgcFIQNPkGbrj6be3gV287Z95Vk0U7MCpkdXV1bXyIjMoEPk3eQmokJsKglOIAD1tm6J4Wo7UMsd9PQ+wBse+CvgzzzwjAe+TjvRSIwGGnB4qS02F4WohE8W58zk94CGzKP6XX3652+VyNdN6lFHh6pFxUgKDkQAgDygWxeDzDKZ47DJeV3PvvtfpVQChJgoKs80C+8Xf09NzRJopEIt0qZQAIFfou0GpXoUIVv0uxxHqq8ov+cxzyDShmsFut9elcmCyLikBloAKuZEm71Jsi/vdKrMi4GqTbIMz8cjgw0yK1OB8SKSfagkMgDwVlZMGDzg6oJR9tIFh5lmzwdEMR/pPnjxZiwjppATSJQHVJg/QRaffm3wT9Gvg624GswPgRsXhNLh//fr1u2nRFYCXTkogbRJQbybCXEl2diXgCzh2bthNFQHwAZAz4BgEgEaijz4C29zZ2VkjzRSIRbp0SiAIOT7MgCnExDeyThS/s7uma+vaZqpANFHUbusBZ8i9ra2tWyXgqozknzRLQIMcF56JOiLc19O6lYrB1hmgvVGVCDj2VQ1Ovq+mpmaLBBwikS4TElAhx7vJE55dIWhb6rZQH6G9WYNrXRYBh/ZmDe5buXLlVj85LacMSAmkWQIa5Im0Q4x2bXkUGpzhZo7VWsIBrp4JGzZsaCc7/KDU4olIW+ZNVgL9kMe2x4P2d+dB+86X8NFP1uARAUffWIPDnvHSdOE2CTjEIl0mJRCEPA57nAj3dzXj468qr+SzDa51V9TgiGTAcTZ4yQ7fLAHXZCUDGZSABnlUm5wgba3dDFZpE00Uraf6Bx5YveNM8C5fvnzbJZdc4iwuLqYH7Yavq+ytURq70rRIKIvEGlmZAYswDjZCRBchLUJ0ULeGqYzaQL8AfEj/PA5nz8u/Zw3O2ntAC+EAR0bVnnn33Xe7Gxsb35gyZcqlxhR9mGj/oU7liWfrlPZOd5jRZGfUbK9bmUnPGIYIeEB3B8i1PyUKBHTo+vPFEYrcfpR6orYfR6NZmiUQ8Cs9XU1vbDiyEysI2f5myLVe6wFHAqSlanDyPTt37nz+9NNPTxngv/3DHqW5lV4tkGPO67ErPi+9pgw/mYAGfjyO8zJo+vL6dH2dmc6vb1/fP31/9Pn1+7HK69P15fXt9eUP+LxKR/OB5yk7flrFOfABNehtcCSKgHuvu+66t2n5bGtk7TGgvpg7uQg3BmW22BSTGa8pIwehx+s4L3wxzOXFOM4j+sjHecSwmEcMi3nEsJhHDIt5ENY75IXjMhxWI+P4E6u8Pp3bYV/fHsWDRb/f1Vq3b9XblBzxAhNFowEOte+hlYWO+vr6f6QKcDSaq06F3FQYdeUEow9fDGfLmMU+ieFI/RPziOFU5Y9UT/T4gOJ2tP/D7e7Bmz+hwcNeYKKOcIAjHiaKZqa8+uqr6+l9KYgf9g6QG/sgxwHXbxAQgyCG9fmGal/skxiO1B8xjxhOVf5I9USLV8j+7mjd/Rz1RzRPwGuIiwQ4zBScFaDas3Tp0r0dHR2HpRYPyo8hD+7Jv5mUABj0eeyHjx58Yh+1y4CDVTAb4qIBzpCjEjfNiW+Qd+775dcPeTRdI9NCf+OSlQl98M3RvAFM0sbmCVhNCHAcSah8TYuvXr16PT2MjAql65OAapPjXXzRnP4iCnk5Llw5ToMvhsPlzYU4cQxiOFLfxTxiuC+/3+/xNB9/cz3tito7rHmCIpE0ONJwRrAd7l61alXjkSNHXpBaHKLpd5hZMfELJ3FA9Buy8oESw/p8vC/mEcOcnmu+OAYxHGkcYh4xTPlx38DtaHnhZP3rjZQEDR5xehBF4eIFXDVT1q5d+whp8YhnS7DK4fdXhdyEd/FJl04J+ANef3PDpkeoDTZPkgIcfR2gxe+7776aY8eO/VNq8dDD2A95sjamLE8/eSTggRsuLj2Otn821D5fQ4lxaW8cpWgaHOnQ1pqZQmHXU0899VePxxPWoEeB4ewYchwadhzmw4V4jhPDnJ6oL9YhhuOtRywjhuMtr88n1iGGOZ8YJ4Y5PZKv+H2BthOb/0pl8F5mEfCoFkUswNEHVICLTdVMufPOOw+QFn9TanGIJtTBHjeSucIHCjkQZsfhSOmcL14/2fqSLa/vZ6z6YqXr68M+1p24nK1vHq3++wHaZfMETEaFG2XjARzaWgOcwq4XX3zxYdLiKC9dGAkw5Pqf2czso0OMkRhGXG5u9N5vpb3p/YdpAKy9AR+YjGlJxAs4a3GcPa4lS5bsOnHixGapxUkaEVwQcnqrasYdw80wowMcl/HOJN0gtLfb1bH5yMHHd1FlDDhr75QAjk6yFsdVKyB3bty48UE5owLRRHYa5JgSY8dhniZDPMeJYU5P1BfrEMOR6hHzIBzLcV8j1aePR31cRgxzPjFODPel+xWvv6N5x4OUhCWoYA8MxqW9KV9cJgryAXBocQbcdeONN+6kd4k/J9eoQDyRnQq5se+Fk3yg2UcxDvcdULUmjotcbeQULhtvffr8XC6Sj5a5TORe9KdwXq5PXz5KOn0WUHH2nnyudt/qnVSMtXfMqcH+xuMHHGVYi6sXm2jwpptuWk4PJrfLNSqiSEPDGuShSTImggTUNSdee/uxA2uXUxaGO27bm6uNxwbnvKzF8fOABp2vvfZa89atW/8oLzhZRJF9zVyJnEWmCBKgb14qPZ01f2xv3o03VsE8AXNx295cVSKAo4yoxVXIFy9e/Aw91rZLXnCySCP7Jpo+NNLnPMQvHXAYfjz/UDuXEcNcVowTw5yeal9sQwxHakfMI4bF/HhiyuPq2LV/293PUB6GO2HtjfoHA7g4o4LGnWvWrLnL6XT6pKkCkUZ3gNxAL4HnA4rcCMfrOG+k8rHS420n3nyJthcrPxgK+D2+5oa37qI+qHyRj4vLhLU3xpAo4CjDgOOMUrX4HXfcse/AgQPr6I20SJcuhgQYcvVijS++pN938RpQHD0n1h378Cms99Zrb7CXkBsM4GiAIVenDGnfccMNNzzU0tLSKE2V+OSvmiuYXZFOkwDmvD2e7saa/X99iCLxOBoAF7W3ljfewGABZ1ucpw2d7733XusTTzxxE33+xCNNlfjED3vcqELON2WGr0+WCS03cXtaTmy6qbutppUkyHAnNO+tl/xgAUc9DLmmxWnacAeB/hDdANK3I/cjSCAIebi3d0QokKfRZHcrvZ01D9XtW72DhqjX3mBtUC5ZwGGqaFqcws5LL7109dGjR9+WN4DiPx7DHXLc0HE5Wt7es/m21WCob4PiTOimTjiJJwM46gPg2PiCE2ee/Wc/+9lvyB5vkvY4SSNO12+uxFkgT7LB7vZ6uptq9678DQ3JThsYggkAppgvCg7OJQs4WkUnMH2CMw6dc9ANoJNPPvnkL8ke90p7nCQSpzPS9CFscryHbzhsEEvA7/a2NLzzy46WXSdpV+WH/KQuLFEvu1QAzrY4mypqJ+lVE9u3bdv2Z9jjEnIWd2wfkBsM+W+T9813093K6j/X7l+9nSQjwp3UhaUo5VQAjvoY8gGmysKFC1fSgqxX3G6ckNLFKwEVcu3rY/k5swK729Hb9Mqed29fSXLRmyawCAZ9YSnKOVWAo06GHDTjQgGdti9atOjXdNH5noScpJGAU00VI74+ln+OXv2gOJ0t7x3cduevaXQqJ+TztGDK4IbkUg24aI+rkNNXIrquuOKKG+kBiYNyURZEHr/LR8j99OFXt6v94KH377/R4WjtImkAcBFuMJQS7Q1Jp0NFoHNiBw0Eube2tnbT/PnzFzz3UtMIA76mJV1cEjAYcIhInLgTkuMOZonH3XW8dvdff9zZur+JhtNLGwMO8zal2hviSgfgqBduAOhki7u6u7u3NHWO+yxNidkk5EEhxfM3CHmfSHN0zQq98Fjxunta6w+v+9GphneO0Wj0cKdUc7Nc0wW4qG608AcffNBrMlvfLx0x5XMGo7lAQs6HIbbfLytNnLELZUkOrO2mF2b2nDz64rX1hzccpG7p4YbmBuApd+kCHB3lI8G+2vnOlr0dBYVV+4tKxl1MswWW/gOX8rHlXYUsq+C8ChaeZv8/vOqYvo5hb2l48+d1+9fiNrwId8rmuyMd7HQCLrYJyDXQ20/tOGUxF+6wlU1aYDQWFPGBEwvIcHgJ9MtKE2f4jFkQq9rcnu72xrp//OTIgccx181wY8477XBDBJkGXAO9o2VPm+JzbioZMXWewVRQ2n/g0C3poklgoKyyc57cTxeUXnfHCVrXfU1D7fr9NJ4e2gA4w530OpNoMuK0TAGO9ljlaJB3tVd3u1yNb5ZVzPy40Wyt7L+Y4u5JP5IE+iFnsUbKmfl4zHN7nG3VdXtWXNvU8GYd9QBgZxxujDyTgKM9OAZc9e1dDY6ejoOvl1fNnm0yFY1TaApR/QhoMK/8G0UCGuQGEmUWKHK83jhA89z0gvoPDu1cfn1b864T1H29WZIRzc1iyzTgA+CmTqj7Lkeru6156xsVoy+cQk+fn44DJyHnQxTd1yBXRRk9bzpTsSrQ7/MoLvvJN/a/d9uSno5jLdQew40bOVghmFG4Md5MA4424UJA97rtvub6f71VPupcq9lSNttgNBLj8oZQUFzR/w6UU+ZVOeD2eV2B3u7ax/a9e/PvXI7OTuqxCDcuKDMON6Q2VICjbYacJ/jpHYte/8mjr35gtVUdLCwaPYfmyunDlFKbQ1ixXBByiDRzTl0RGPBiPXd7S8Pbyw68d+/TdAz5YlK8QzkkcEMSQwk42mfI4Wugt53c3uB0nHyttHz6THo4dywOnjRZIK7ojiHPxOw4lg4EYJI4mnfW7V95ff3h9bupd9DarLlhkohTgZk9+/pElS2AA27eVOjt3fW9p4699kr5qFkmc0HZR6TJ0nfEYngDzZUYmQeZrN6ZhEnSeXj1nk2/vr2nsw5vn4LGZrj1i6cG2VLyxYYacIyAz2zW4hro9HPnO3nstZ2FhZX7Cm1j5tCDAEWkyqU2j3HctV+7FJvjWE+CWRKvt6utpeGtX+7f/vv/6zNJGG7McfPFZNpuv8cY/oDkbAAcHRIhF0FXw21N2084HfWv2UonjaHPhEwJaikJ+oAjqdvRINfFD2ZXfSILF5I+Fz2kUP/akT0rlhyv2bCX6mKNDcD1N3CgqIbc4RzPJof+YOoEJx7eioNPl+FDlHSxqdgQnj77h5+oGPeJXxQUlE3Cg7qZ+EmmdnPWYYYjGRec/nMrbnfnsbaT2+6v2f3wZqoPJghDzVOAvNwVDbLCSqbplJTNNsAxKP5hBeR4OBGfSQDkDHpRYWFFyYzzf/Gd4oqpV5JGt+IZxlRqLGorr1zwmdjEmOMZEp/X4erpqFld/f4Djzud7ZghgabGBrDZ1sYsCa/lTqwhKphOly0min6MLCT42KAV2Kbzeb1Ob9Pxf+32utteLSqZOJ4++jRJmi16Efbv95/8rDsi++pzFX3mCM1kvXPkw7X/Vbd31eskc3H6D9pbhBvHJ7mfiv7upjSUjRpcHCD6xyYLa3PW6DBbVM0+4/yffKq88iPXmq0jJuOdf/J2vyjC/nBQk/fviyHRzva6u462N+96qHrng29RHtbUrLUx9cc3bljpsEISq8yKcLYDzkIC5Aw6bHNAzva5CrnZbC6c/pHrLykbefYVZmv5NAk6iy66PwBsV8fhrrYDj1Xv+uOr9GYyBpt9ntcWbe2s1NriiHMFcPSZtTlAhzZn0AE4ww7fOuP86z45ovLcKyzWkecEL0RN0kYnwYguaGP78MJLetl8277O1j2Pffj+n96mPAAZG8BmHxobYPMdSYCdtVqb+qa5XAKcO40+49qBQYc2Z42uAk77qj919tUfqxh1wZXWosrz6cEKslxQbPhOLwZNFKz4I7D9broL2fp+e/OO1TW7H3mPBMNgi75ojgBqvpCkYG64XAQckkW/sYlmCzQ6Ty2KoBeccc53Z5eP/uiXrIWjFpjNRTaD+no0FM1/2DWo6cIRb3D1eh12l7P5jY5TH6yv27cGt9cBsQg1wtDWvIl2dk5obeq75nIVcB4AQ86gs+nCoLNmV7V8YcnY4ikzvr3ANuKMz1mLqi4k0E3q+7nVu6OoIn+cOv9NUyJ4+ACfBKG3t263d9a9XPvh2jecPSdxg4a1M4BmyBlqnvaD1s4ZcyTc0ct1wHlMetBhi7CNziYM+6qmrzrtwtHjJi/6rK1k/OfoiblpAJ1hz0XNzpoai6AANTafu/uwvafh5cajG//ZcmL7KZIJA8xwiz7SoK1ZY+c02DQO1eUL4OJ4grZH0E6HRmetDsAZetE3T5q6eHr5mPPmWQurzjcXls8i0K20VFcx4iWYeA9JFpoyA4CmJatYI0JQu7zOjr0uZ8v7HU073zlW82w1dR7aGPAC5nA+0llj8z2HnDNFaAxhXb4BzoMMUtlvo0Ojs1bXA69qdEqHby4sLLeOm7p4Vln5tAsshRXnFxSMOJseirbgAhXPjAZvmrDYgn7/jRRuPjV+EGLUxbzRBSKWqdJ7RnChGKBPftAt9AMeZ/v7XR2HdzTWPLvX6eyAycFQA2jeGHBOY23NGhuNcEMUzA/HRyo/RhM6ChF0aHbRVhe1O0POceybiovH28ZNW/SR4pJJ55oLiieZzLZJJottPFY2BoHHWnWAT1Wr0owkUn18JJYoHv9xUQiQNd/roJfnNPi89mNed++x3p5jexoPb9zV29uAu4qAlDUx+ww2fI6Dz0CL9nWkzlD23HZ6qef2aKL3HmNl84VBZ83OQEfyOR98lDWOnjB3dFnFOZOttjGTLIWlk81m20RaMlBpUEw2Ay2QoRPARg1SffQXF7F9vtpFaOEgxbSrhuhDAV57gBZ+BBSf3e9ztXq99uMeZ/dRl73pWFf7vqOn6jfBhmYoRe0rwhsuLOZlu5p9tTv5/Gc4Ac7HEWMWN4ZW9AE6Q83Q8z6fHKKvQq+r10DmjrmoZEKx1Ta6yGItK7aYy7AiUvF4u+weV1evy37K4eip7yWzAmBCi4obwwyfta7oI8xAM8TYF/NwWbHevNXWNPYQNxwBF4Uggo4wg8q+CL0IuAg350Ec18H1oi0xjH3RMXiI4zBrVwZcDyxDy1DzPudnn+tjX2x32IQhfOmCEmBZMJDwGXQxLMYBbqSxz5AjDg4+b7wPH9DBMXz6fUCKOEAs+gwv+0gTw9jHBsd+cG+Y/uUDMUyHH3XYLBsGNJIvQq3PgwbEesQGGUDRR1i/Mez6eHEf9WJfOp0EWPi6aLkbQQIsLwYZ2aLFiekRqhwAJkPK8KJMtLhIdcr4PgnwwZECSU4Cejnq91G7Po7BFVvWx+n3xbwyHIcE/h9VLWRYHWXC/QAAAABJRU5ErkJggg==', // @suppress longLineCheck + + green: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAAC4CAYAAABQMybHAAAltklEQVR4Ae2dCXQcxZnHR3NoNDp8SD7kU7bxFXCchBhMYoLNmhCcOBBykGw2gYTkPV6AhGXD2sTZJQcJG3jsgw3hscuCsTEsOAQW1sbY+MAHxpYtHzI+5EOy5UMStnWPZkZzab9/j75WTWt6NKO5Z6r82lVdXV1d9e/ffPq6uro7zyBDIhXIi7DyngjLyWJRKhDpCYiy2pwoztrpxSwCb+d1bayFm9f1Yu3+cj2MAgOJH2bXnNnEGiHWppW8d999d/inPvWp6YWFheNMJlOR0WgswdLT06PElFdM+xbn5eWVQDXK76TI7vP57H6/v5PyOxFjobwuh8Nx4dixYye+9rWvtaK4ZqFVNY/TiGUIoQCfsBCbcjaLNVHgJRUQG3/4wx9ali1bNmXYsGEzrFbrdLPZjGUagTyNAB2ZCLXoh3CJoD/p9XqxnOju7j7R1tZ2/LHHHqtbtWqVh47pp0X8AaAZbPmRzvnAJzPXhRBhVoBeunSp7b777ruuuLj4xoKCghsI5s8TcPnpIBT9oNwE/D6Xy7Xdbrd/8Oyzz+5+/PHHndQ2LfA5D3suA85QGwmMPLLQ+b///e/nkIW+0WKxLKBlLuUXpAPQEbTB5fF4KmnZShb+g0ceeaSKLLyb9gPgIvQRVJVdRXIN8CCo77777oI//vGPt5CV/n5+fv5NdGrhK2dDsLvd7k1k3f/n17/+9frly5e7qFM5CXsuAM5QI4a1Nh4/fvza0aNH/4PNZvs2rZfRks2h2el0/u2TTz55dcaMGXuoo7DoolXPajcmmwFnsBWoKysrJ02fPv3v6QLxH2hUY1o2E63XNxqhOUkXqq+eOHHitblz557pBV2EXW/XjM3PRsBFsE21tbXXk7VeCheELhKzsb9Rw0cXqT1wYciqP37FFVd8SBX4aMlK0LPphKMvirWm2FRfX//lESNGLKXRj3m0LoOOAjQas/Py5cuPV1RUbKQiDDrDrrNX5mRnA+Aq2OPHjzfv3r3766WlpUvIDfl85pyG1LeU3Jd9LS0tT1x33XVrzp8/76UWAfKMBz2TAUfbFbgJbAuBfUdZWdkSuvFyZepxydwW0I2lo83NzQD9rwS69mZSxnUsUwFXXZHDhw/PmTJlytNksedknPpp3GCy6FV1dXX/OGvWrCpqpui6pHGr+zct0wBXLDZ1w/TSSy+V3n777b+j+R8/pYtHAC9DrwIO9xHD5c5XDF5fS0ya0MWo3+nwvrBx47nfLLlvKypj0DNmaDFTAEc7GW5zQ0PDD4YPH/4YuSMJmQMSExVpsPPxhjsMHt/FuLWkp8dwqb3dt2zhnD2vUKXsnwPytAc9EwBHG01YDh06NIuGtf5DjoyQGmHC4XMLwmwd/Caft2fnmXr3A3d8Zf9hqgXWHEtaQ57Of9pViz179mwrXeE/MnPmzF0S7sEDGuueJnPevCuusO76sPq6R2bPHm2l+sy0gKG0NZTp2jCG27R27drx8+fPf4lu1NwQ6wnKlf0TZcFF/bwe//Z9uxw/vvfuj89Tftr65ukIOI+QmE+ePPllGgJ8UfraIloDp5MBOFoB37zxQvdPvr5gP24SsW+OsfO0CenkoqhWe8GCBQWXLl36Hd1de1vCnTas9GtIXp5h5LgJ1re3H7z2dwsWjMTUYlwrpZXLki4WXIV73bp1E66//vqVNClK3mLvh1RkGcmy4GJr3B7/zkOVXXfd86PD5yg/bVyWdAAcbVBGSY4cOTKPbtqsJqs9QhRPpqNTIBWAo4U0l+1yw1nXd29duH8nrabFKEuqXRSG20yz/hZPnTp1rYQ7OpjTqTRNUhwxtsK69t3tcxZTuzDCAsOVUiOaSsBxbAhgoZl/d02YMOF1SttokSGDFSCabeVj819/v3LOXdQNCy04xynjLFUHVuGmu5K/HDNmzHN0ux2/eBmyQoEe84gRluc2V13zS+pOSiFPBeAK3PQQQj7NQ36CZgA+Kh9EyAqqgzpBQ4h5w4aZH6URlidwrmljSix5sgFX4F60aJGNXmzzYklJyf1BqsiVrFOgqNh0/5ubJr24aFEp3M+kQ57MCwAF7pEjR1rpBs4KeuD3W1l3NtOkQ6kaRQnXfZfL/+Y3bqz7Ed3f6KZyPIwYbpe4bEuWBVfgphbn19TUPCnhjsu5y6hKCgqM33pr4+QnwQAtSbPkyQCc4bY0NjYupXeQ3JNRZ0Y2Nm4KFBab7tlSdc1SqjBpF56JBhwuEI5hOXfu3N30rOS/xk0tWVFGKjB0mPlfN1bOuRtM0AI2EuomJxJwNBx/iix0QfkNupJ+mtIySAUMpSPyn16z5fPfICnYkicM8kQBznCbq6ur59Pt9+U0FAjYZZAK4J6+aczE/BffWn/1fJIjoXc8EwE44Ea95vXr18+gt0mtpjQmx8sgFVAVIEgKJkzJX/2fq66aQZmAHMzE3ZInCnATPRBcNG/evJU0t2SI2iuZkAoIChiNeUM+O6d45cLbxxVRNv7Cxx3weFeIHwwaar148eJTQ4cO/SmlZUiyAuk4Dh5Ogs5O3wsLPrfnQSoT9zHyeFpw/FhQn+Xo0aPflHCHO6Vym6hASYnpp29v+dw3wQ4tYChuhjdegDPc5jVr1kybPHnyM2IHZFoqMJAC48Zbn/nzi1dNo3Jx9cfjCbh54cKFRfS42Qrpdw90OuV2rQLwx6/9QvGKhQsVfxyQx8WKx6MS/EgUv5vmGTw5ZMgQeadSe/aSvJ5pPrgoj73D91/zr97zEOXFxR+P1YKrrsm+fftupNmBEm7xbMl01AoUlRjvWb1u9o20Y1xclVgBV6w3uSVFNN79JL3LLuoOyR2kAqICYKhisu3JBQvG8tBhTIzGsjNbb8vLL7/8C3oxz3SxoTItFRisAhaLcfqyP435Oe0f86jKYAFnuM2vvPLK5FGjRuHRJBmkAnFToLTM8tCfnpk5mSqMyVWJBXBcWFro6Zw/0Z+Vwrj1TFYkFSAFwNQX5w/5N0rGNCFrMICz9bZ8/PHH+MbkV+UZkQokQoGiQtPX/rb+M7f0Qg5WwV5UYTCAYx/zrbfeWkw3dJ6I6miysFQgSgXGV9ieWHjrqGLajV2VqGqIFnC23ua//OUv99PnriuiOposLBWIUgGLJa9iya8q8HA6Ax6VFY8WcJQ30Sewh9DrHn4WZVtlcanAoBQYXmr62fe+NwGzUnHdFxWz0RRm621ZtmzZT+hF9GWDaq3cSSoQpQImU17ZnfeO+gntFvWwYTSAo6wZN3XKy8vvjbKNsrhUICYFykZa7r1mwUjc/GFXJaL6IgWcrbf5+eef/wH53uUR1S4LSQXipIDZklf+m99N/AFVx4BH5ItHCjjKmehbObaxY8fiDpMMUoGkK0BW/OezZxdH9YasSABn621ZvXr1HWS9Jya9Z/KAUgFSID8/b+KjT02/g5IR++KRAK5Y73HjxuXT8oBUWiqQSgVGlVseoCnZfHdzQH4HKgDrjcX8xhtv/B1NqJqays7JY0sFLPl5U59bVbEQTNLCfOoKEwngGHs0013L78npsLo6yg1JUgAMjhlb8D0wSQvYBOS6YSDAsd30/e9/n54hHrpItxa5QSqQRAWKh5gWffWbY4bSIQF4WIbDbcQvA9vNS5cuvY2es8TVqwxSgZQrYDQabHffU34bNWTAIcNwgGObAjh9P+c70j1J+XmVDehVACyOLs//Dq0y4Loc621g59301FNPTSwsLPyiVFcqkE4K2ArzvvjPv52GIWv2w0P64uEAV6z34sWL76BfjF65dOqzbEsOKQAm5/9diWjFowZcGT2hx9G+nUO6ya5mkAL0WBsDzla8X+tDWWa+uDStWrVqBo19T+u3l8xIawUsplFp3b54NY7mik/703/MxNtpeTSlnxWHk64NKuCf/exn58uLS6086b8+3Pqg4WDNHw0O5yf9Gkuf9+sX6N3twXmaVWwMkUWv+Q7eLVShHk1mv310Kg9Vrt/h/PStQoN/PlVxhBYYa7AbVCwc4PQxzxHXS8ANhkZ7jaGq8W8Gh6ed9MuM4C2ebrD7Jhp6CIJsDr481/UGw4nnqY8MeFB3tYDjF6BY8KKiIjO9qeq6oNI5urL+1L8bOt2XM673PrPf4OjwZDXk/p6e68BqV1cXAx5kxUP54MgzrVixYpbJZCrNuLOagAZnItyQwWQ2GgppXlKeEec8OwON75V+/YErZlHv2A8P6qieBTddeeWVX5LuSZBWGbnCkDs7PQa/PyO7EL7RZI5HTCj+EhXaTwt7IKpfprXgintCBU3kf8+TgIfXNlO2AnJbicVAt7izLoBR2xDLPOoYW/CgP1eiBWf6TXRrPp/877lZp0YOd4ghhyUPNUKRydJYbaa5IyYU5l8+53BRP5hjxYprf9NYNz7zzDOfpl9GSSZ3Wra9vwIMORm9rArUn5Kbfzzt09QphV+xcyEt+Pjx42dK90SUKXvSDDksedYEwnrYyIKZ1J89tOhacN5gpLnfU7Om87Ij/RRgyLPJiFlsZjALC84cK/1GBgdswLqJZg9OzabOcwdl3KcAIC8oNuMtrn2ZGZpCHyxW0xXU/H4Xmgw49xLrRgJ8Sob2VTY7CgVUyLNgnLwXcIXfXgkUpvv54KWlpeaCgoKKKHSSRTNYAQXyIrPB1eXVzOLIrE5ZrcaK0lKbuaXFCbDZYCsuCfcEmcbf/va34ym2cqaMs18BhjyTZ/3TmKB17ncngV1Y8X6AM/XGq65SPsaZ/WdV9jBIAUBuLSSfnPFgIjIoHj2pCFO7xR6oFpy7YRw5cqS8wAw69bmz0gc5cMiwQE0uKrH0G0kRfXDFQbfZbBMyrGuyuXFUQIGc3p/Q7fSRT65O6YjjERJXVX6hCewqHPNRsILAFjyP3j2Iz0XIkMMKBCA3ZdwQosloBLsqyziFogXHBiNNkS3OhrFRdE6GwSsAyPPJkrvJkmeCHVeYNeUBcPbBlc7ziko9AY6XjMsgFVDmkysXnqAjAwIN54NdlWWkRQuOLuRJwCGDDKyA0ZRnsNrM5JOn/zi5yZzHgHPz1VEUZCjkS8BVbWSiVwGGXCEkjVUxGlXA1b85bMHVDAl4Gp/BFDaNIXe7vGk7uEL+iOheK0zzKAqkkxY8hQBlwqEBeX4BJmilZ2uNRuX6UeGYW8gWnNelD85KyDikAgy5uzv9xslNRvUiU217PwtOW7TQq4VlQioABRTIrTQzNc1MeU9eD9gNacHVPzper9dBH3mVj6tJlsMqwJB7yJKnyzg5vTXAITRaYZrHwdV8n8/Xpa7IhFQgjAKAnOZhp83gSo/foGVXGQfHD1D9EcKCh+mT3CQVCFKAIfe6yZKrFAUVSdqK39cjsqtwLfrgaEiPtOBJOx9ZcyBAbs7H3JUUd8mnWPCgn5l4QalskICn+CRl6OEVyMld8brp9VkpMuU9fj+7KCrkIS14v9fpZqjostnJVYDuJJIlJ6RSYMrBrK9HAVyFG71nC45MZZE+eHKhyLajMeQ+jz/phtzvy4MPrrIMbUULzoDbs0102Z/kKgDITRZj0g253+8Huwy40mm24KoCTqezBeZezglXJZGJQSgAyA0EOSx5MgLcfp+7p0V7LLbgTL2/tbX1tLaQXJcKDEYBtuSD2Xcw+zg6u8EuflHMs+KiYAVByTx58mSdvMgMCCL/j12BpEFO9Laed9Yxx70t72ELjnUF8DfffFMC3quOjOKjAEOeyMEVfOyqevtFLeD9RlH8GzZs6Ozu7r5E81FGxqd7shapAI1mwCen5zz93sT45H5Pz6UTey52ktb9XBTor1jv3o1+u91+RropkEWGeCoAyI0EebyHV8Bqt8t7htoKuEMCjn4AcqWAw+E4jQwZpALxVkCBnG7tK5DDZ4nT4nb5wawIuNJ09sFFC+7DSIq04PE+tbI+VoAhj5dPjiHCbrsXgNNTGMEWXBwHVyFvamqq48bIWCqQCAUAeQ8ZcJoBGHP1+KF0NHvALCw4c6zUG8qC+995551DNOkq9iPH3HRZQTYrgJuJmKQVa6CvOffUfNhwiOoRXRSFXwYcx0AGCvjoI7CX2tvba6WbAllkSKQCsUKuXGB2eWsr37twidopuihKs7WAM+Te5ubmSgl4Ik+trJsVYMgHMz0E/ndXm6eS6qI3E+m7KHwsxYLTiq+2tna3BJxlkXGiFQDceDe5EiMd6UIPzLU0OneD2d4FDKtBz4L7li9fXkmzs4IKq3vJhFQgAQow5NFUTYT696w5DwsuuieK/416QgGu/BLWrFnTSn54jbTi0cgty8aqgAo5rj0HWHB7vtvhqTnyUVMrlWYLDrhDAo62YQOsNvwZLw0X7pGAkxIyJFWBgHsy8CHhf9tb3Pj4q8IrxWBXhRs1iBYc6ww4fg1e8sN3ScAhiwzJVoAhJ1dc/2YnNaq5oWsXRQBcdFHU5oo3epAJwBly79NPP73npptuchUVFRWoe+RgwnXRZmh3YBQqu4OuMQuyiX0a6GQHCuhs1D1GX7VBKVhp7APgtfvSS4dcm1bUsQVn6x105FCAo6Diz3z00UedjY2NW6dMmXKL0ag19kHtiHil9nyj4b2dVYaOLvEVFhHvnpKCXs9XDUa3m44dpF1QW7TiB23UWdHdR+cw8DlDBlCgE/S30A5h9tOpLubsaG/r6JWnJ+gNrtbmrRdO7sYMQva/GXK1nVrAsQGaoCDMvufgwYP/N2nSpLgB/sJb6w0tHWhTZgV3t4teidBNjYbkkEhPem2/uCyjpt1fu127f7LLa4+vbZ+2Pdry2vWB9tdu1+6vPV6gvN/vMzTUHv8/Ku2hBaz2gxs1hTLLqIEB995///07aPpss661QS1RhEyEG93LtxbQKxH4+7gQPdLAZRGLad5fzOMyYoxyXEZMi2XEtFhGTItlxLRYBmltQFkE3ofTSmYE/w20v3Y7H4dj7fECrorP42mu2rZhB23VvcDEnuEAh9n30MxC5/nz59+LF+A4aKaGAOT5wbxpO6M9X9jOedqyqVjntujxo21Tostrjxfheldnx3tuu91JxWHBQ15goqpQgCMfFpytuGfTpk3v0PtSkJ/zAZBbLL2QMyRiDIUYCjEtlkllWmyTmNZrk1hGTMervF49YfL99JbNpvrat6k5onsCXvsFPcDhpuBXofjhS5cuPdzW1nZKWvGAfhaGvJ+cMiPRCoBBj8t16tCOTUfoWAw4WAWz/UKoi0wUQmGGHJW4aUx8TVlZ2YP0DR9sz/kAyBG8HsgjQ7IUAOD2jvY1dDwMa0F8hjsk4HoWHO2FyVet+MqVK9+hh5Hl2YQyvQGQm/PJXQkXcKcCge9YcFrJDPFftOVDVJFWWdH2Z4Dy9PpjT92R/e9QH8EiPAwwGtI9oXxdHxzb8ItgP9z90ksvNZ45c+ZdOf8K0vQFC42sKJAzwNoYRfmkiWltOV4Xy4hp3p5psdgHMa3XD7GMmKbyALKrs/3dMx8faKQkLDgAB6MhrTflRww4fi3uV1999QWy4rq/FlSYi0GB3GLJxa4ntc9+r9d/5tjHL9BB2T2JCXA0PsiKP/nkk7Vnz559X1rx/ueVIQ9z8a+OJMsygYGmaHTAXVdnZ+f7x/bsqO0FfEC4cZbC+eDYDmutuimU7v7rX//63x6PR/dPAnbK1QDITcoQYq/fDSHwp5hjMR3I7b9d70+3Xj7XPdj6Yt1f266B6htou7a+3nW6c9lTf/Lwf9PuuJ0suidhPYqBAEdzUAEcecVN+cMf/nCMrPg2acUhTf9goYtOk5ncFT5RKII0B07rbedykcax1hfr/tp2DlTfQNu19dE6Rk4c9o5th3d+cIxW2T0Je3HJ1UQCOKy1Cjilu9etW/c8WXGuQ8YaBVTINflydXAK+H007+TUyedpb7begA9MDuhJRAo4W3H8erqXLFlS3dDQsEtacVJDJ0jIdYSJMhvW29nVuevAtvXVtCsDztY7LoCjSWzF4dgDctfatWufkSMqkEY/AHIzja5gLjMHTgcm9AfyOQ9lOM3bo43FOsS0Xj1iGaQHCtG2D/XxPmKa2yPmiWne3uP3+Zvqjj9D21y0gD0wGJH1pnIDXmSiDAIAD7rYfOihhw7Su8TflnNUFH10/zPTRafJbFZOMp9ojrETp/mEinm6lYbZEG192vLcDr042vZp69fuH247psR2tDS/XbVl/UHaj613RKMnLFEkLgqXZSuuXGzigA8//PDT9GByK/6MyKCvAEOuX0Ju0SoAprzd3a3VO9Y/TdsY7oh9b64vWsDZF8cBXZs3b75UWVn5Z3nByXLqxwHI5c0gfYWCt8B6Nzde+HPj6dN4VhDuCZiL2Pfm2qIBHPuIVlyB/Pbbb3+THmurlhecLKl+DH9cHULkYuyfI45kwX68j5jmfcU8Mc3b4x2LxxDTescRy4hpoTwezXN1dVVvfeuVN6kIwx219Ub1gwGcrbhysYkGrFq16jGXy+WTrgokDR8UyE00iZNPKIojHWngsnr7D7Q90uNEWi7a4w1QHgz5vF5f3ZEDj1ETADdfXEZtvdGFaAHHPgw4flGKFX/00UePHDt2bDW9kRbbZRhAAYYcWMslWAMDPcxgb768mm7qYL631nqDvajCYADHARhytuLOBx544NnLly83SlclMv0BuZFGV2ToU6CH4HY7nI37Nr/3LOXicTSt9e4rHGFqsICzL66Oi+/du7f5tddee5g+f+KRrkpk6pvplr4CObsbORwDKBpy9pyuqX74YkN9M60y3FGNe2uVHyzgqIchV604DRvuI9CfpRtA2uPIdR0FFMjlU1L0pQcvjZo0PHvggw37SCqt9QZrgwqxAg5XRbXilHbdcsstK+vr63fIG0CRnw+GPFf9cbpbaejqaNuxZfXylWCod+G7lmAsJYDjDOLgWPiCE788x4MPPvgb8sc/kf44qRFhCECeez45/O5up/OTqo3v/oakwuvOwBBcADDFfFFycCEWC85HRCMwfIJfHBrnpBtATa+//vqvyB/3Sn+cFIkw4Ja+URxCzHKfHGaZ/tJ76SmdX9FrIJpoVeGHYrDEw4KUHHyIB+BoJxrDrorSSHrVRNWePXuegz8uIY/8BCmQG7P/zQVgAn735aYLz+3fsq6KFBLhjunCUlQ7HoCjPoY8yFVZtGjRcpqQtdGtvLhSPKxMh1MgYMkBefZ65TRJ0NDZ1rpxy2vLl1NHta4JDCaYijnEC3A0hCHnURU02rF48eJ/oYvOvRLy6M6ViVwVoymepye64yeytI8sd1dH+94tb6z4FzDSu/CwYNzgRh/iqSAAF/1xNNhBX4nouPPOOx+iByRq5KQsSB55YMizyRXHiEm3vbNm99o3HnJ2dHSQGgBchBsMxcV6Q+lEOHtonNjAPILcW1dXt3P+/PkLPth/eGgePqclQ0QK4L3synvBs2BKMmYIuhz2c/s2rf1ZY33tJyRAFy0MONzbuFpvCJwIwFEvQhDo5It3d3Z27naYCm6mGXWFPNE9UFT+H04B/vhAgPHM9Mv9fvpglNPZfGjnpntOHzl0lvqrhTuulpv1TBTgogVX0wcOHOiix7j2Dx899is0HJYvIefTMHCc1/uFjUwckcL9EHphpv34gY/uq9nzUU0IuGG5AXjcQ6IAR0MZbI6VxjfV17UVlQw5OqR0xEKah2GRkEd+TlXIIW2GGHK86tjtcjnqjx74pwNb38dteNFyx228W0/FRAIuHhOQq6BfqD1+0WIp2Dds1KgFNCRmkz65KFX4tAp5Bvjk8LndDkfriQN7fn5g6waMdTPcGPNOONxQMtmAq6DTnasWn8e1s7R8wvVkyEv4xKFRMoRXQDUIiiGnz16n4b8eGud2d9kbqnd+cC+9bu0o9chOCwBnuHEzJyF+N9WrhmQBjgOyBVchv9xwobOro3XbqPGTrjVZLGV8MaW2TiZ0FQhATlKyqrolk78B49z0HsGT+zatua/uyMHT1AKAnXS40fNkAo7jITDgStx++aKz+cLZD8onTZ1NryEeA59c+uUBoQb6X4UcBdPAJ8dwJt5CRTMDD+xY88YvGs+caqCWad2SpFhu1i7ZgAfBTY1Q1umdz+7zp45uHXfFjCn0AstJeUYJOZ+ggWLVXUmxKcesQHqWkm6/t2zd+saKJW0Xmy5T2xlu3MjBDMGkwg3tkg04jonQD3S60vbVVh/cPmbyFGu+rXA2+eRkyGGWZBhIAdYpYMST75H30Bg3fcqlp62p4eWNry7/N3rVWju1WYQbF5RJhxu6pQpwHJsh5wsN+nit13+quupA4ZChNSVDh881mkw0wiKtOcQaKEAnCJrMoMwIpJESj6Orlaa8Ltv2v6++QeeQLybFO5QpgRtapBJwHJ8hR6yCfuFUzQX6U7d5RPn4K8kvL5cuC6QaOKiQJ8EfJ2/bgItJR3vbwb1b1v3iaOX2Q9RCWG223HBJxKHAZP/+FMHSBXDAzYsCPV18dp06eGBjecVkk7Ww6DPSZVHO14D/sbsyYMEYCuDOpNfj7mlpOL9yw6oXf996sQFvn4LFZri1k6diOFpsu6YacLSef9lsxVXQ6c+d79ShqoN05/NI0TByWYxwWWjAQPrmYc96nz7xNeWBhxRofNvpbDl7rPpX2/73f97qdUkYboxx88Vkwm6/h+28ZmM6AI4miZCLoCvp86eON9ibWzYPHVk+mlyWKXBZMC7WdyI1vZKrvdqwrLEJArAxSoJvgna0XNpctXntkqOVHx6mWtliA3DtDRwYqpQHkJJOAe3BXFr88PCmSist+OKqjZZCpK+55bYvVEy78pcFRcUT8eRL3zAZbZWhnwIAM5bAw3/dXfaz9SeO/vve99fsovrggjDUPATI011xwPj8smJpeO++6QY4mhUwzwHI8Zg5vrQKyBl0W0FJSfENt/39D0pHj73LYrVayXWR1pwE0guBGYjRMaeOkNBDtc1NDSs/XLP6FVdnJ0ZIYKmxAGz2tTFKwnO5ozsQ7ZjIkC4uiraPLBJiLLAK7NP5vG63t/bQvkMOR8emoWWjx9Fr0CZKt0UrYd96nyvHtkM/xhwudkfsra0fHtz6/j/v2/zuB6S5OPwH6y3CjfMT25+KvubGNZWOFlzsINrHLgtbc7bocFsUyz7vq9+6oXzK9PsKCgsraE6L4rb0nVSxutxOByx5aA3Yz/aRn+1yOOobT598dte6N7dTabbUbLUx9Mc3btjosEEKXXkKc9MdcJYGkDPo8M0BOfvnCuRms7lg7uJv31Q+ruJOa1HxVLzcEv65BJ0lDB0z2LiAJD/7VNOF+pcr1/5tE72uhMHmmMe1RV87La222NNMARxtZmsO0GHNGXQAzrAjtn5x0Te/VD556p0FxSVX4Y1RmIorQSdlhKCAjfFsL1lse+eRptOnXv7ovbd2UBGAjAVgcwyLDbD5jiTATlurTW1TQyYBzo1Gm3HtwKDDmrNFVwCndSW+5uavXzNu8oy7CocMuRpfVgi8hiF3hxcDLgpm/GFilMfg6OjYf+H08ZU0MrKXNGOwxVh0RwA1X0hSMjNCJgIOZdFuLKLbAovOQ4si6PlXz7959tipM28rKhm2wGzNL8TrGHLlopShxoQo3Fr3drsdXZ1tWxtO1byzf9v7uL0OiEWokYa15kX0szPCalPb1ZCpgHMHGHIGnV0XBp0tu2Lli4eNKPrc/C8vKC0v/0phybA5NI5uogldivuSbePpGAkB3JifjU+CODrbqlqamjYc2LZxq73tMm7QsHUG0Aw5Q83DfrDaGeOOMBRinOmAc1+0oPONInZfxFix9BOmXjVq+py5Nw8rG/kVmp47lV+XFvDVM8+NUS11H9R0S91xqq350oYTVZXvnzt15CKJxQAz3GKMbbDWbLEzGmwRDE5nQ8ygIwbksOhs1QE54NbG5qu+cMO0cZOmXW8bMvTqgsLiWQS7FW95hc+ersAHA+1XXmRJlrqbXqxz2NnRvv/CmZMfHtm1/ST1F9YY8ALmUDG2s8WGC5IVYFM/lJAtFpz7wzH6xbADdF4AuBZ4xaJTvrKtoLjYOuvaL80qGzPx8wVDSq622Yo/ZTSbLLhbqjwzqsxPp9JKCMiXqBGaAMQ4UMD1xU0Y8jsMmM2HJ9ZpLprH6bQfc3V07m9uPLvv8J4dh112O1wOhhpA88KA8za21myxldqpfFaFbAWcT5IIOvx00VcXrTtDznkcm4aWlRXOuGbeZ4aXjfm0xVYwMT/fOtFsLRhnwsMYyvCjUQG/76KVD62NtVIHoNWWUiAmoHFRCJAVX5pi+oKdk+zzBbe7+6zH6Trb2tz48fG9O6vbm5txVxGQsiXmmMFGzHmIGWjRvw7dGCqc6UGreqb3J1z70VcAzjFbddGVYbC1sVhW+aFUzPzMqNETJ1YUDyubaLUVVeRbrRNMFnOZyWguzAvAj9fToZ6AmwPLjxUKCk1EMltoir30OJOjhyD2+b0On8fb7O7uPtft7Kq3tzWf/eTs2fr6mmr40AylaH1FeEOlxbLsfnCstCeb/2PNs7mP2r6hz+ICeNmycwwwGWqGnde5jBgjjUWsN4/cHfPQ0lFF9PidzVpUWFRgK8KMSIPL2eXo7qLRuvZWZ3vLxS5yKwAmuwgcM8yI2eqKMdIMNEOMdbEM78t1ckzFciPkIuDimQ0CkjYwqByL8IuAY7u4jcujPqS5XkoGpbEuBhE4TrN1ZcC1wDK0DDWvc3mOuT6OxePmTDrXARdPNGvBcCLWgsvrDDEgRzmOOT9UXTgW5wM6BIZPuw5IkQeIxZjh5RjbxDTWsSBwHFjL0f9Z8BztfthuszaIwy0i1NpyOIBYj3hABlCMkdYuDLs2X1xHvViXQaMAi6/Jlqs6CrBeDDKKhcsTt+tUGQQmQ8rwYp9weXp1yvxeBfjkSEFiU0Cro3YdtWvzGFzxyNo87bpYVqYjUOD/AZrbm7Ts1rpFAAAAAElFTkSuQmCC', // @suppress longLineCheck + + red: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAAC4CAYAAABQMybHAAAk/0lEQVR4Ae2dCZxUxZ3Hq8/pnhkGmOEQuQS5VCTxWHEDBlyNkciakMMkxujGuOvHO24IKCae0UQlKwmyroocoqtozGpA4oFiVAQU5IaRcchwDsPczNF39/5/b+bfVL/p7ume6bur+DyqXt31r2//5//q1XvPIJRLpgQMMVYeiDGfyhanBGKdgDirzYvsLLtIPguB0/lc7+vh5vNIvr68Oo8ige6EH6Vo3iSxjODrw1rcm2++2f+MM84YV1hYONRkMhUZjcY+OAKBgOZTXDGVLTYYDH0gNYpvIa/V5/O1+v3+FopvgY+D4tra29uP7N27d98VV1zRiOy6g06DcRyGr1wYCfCEhUnK2yiWiQYvSQG+8ac//all3rx5o/v16ze+oKBgnNlsxjGWQB5LgA5MhrToh1BL0Fd4vV4c+1wu176mpqYvHnnkkf0rVqzwUJt+OuQfALrBmh/hvHc8mfkuCBlmDei5c+fab7nllguLi4svttlsXyeYzyPgrJkgKPpBuQn4LU6n88PW1tZ1ixYt2vjoo486qG964PMe9nwGnKE2EhgG0tDWBx988HzS0BdbLJbpdEymeFsmAB1DH5wej2cTHR+Qhl937733biYN76ZyAFyGPoaqcitLvgEeAvX1119ve/jhhy8nLX211Wq9lKYWtnIuuFa3272WtPv/3nPPPW8tWbLESYPKS9jzAXCGGj60tfGLL764YPDgwT+x2+3fp/MyOnLZ1Tscjj/X1NS8OH78+E9poNDoslbPaTMmlwFnsDWoN23adNq4ceN+TBeIP6FVjbG5THSksdEKTQVdqL64b9++lyZPnlzVCboMe6SiWRufi4DLYJsqKyunkraeCxOELhJzcbxxw0cXqQGYMKTVHz399NM/pgp8dOQk6Lk04RiLpq3JNx04cOAbAwYMmEurH1PoXLkIEqDVmPV1dXWPjhw58l3KwqAz7BFKZU90LgAeBHvYsGHmjRs3/mtpaekcMkPOy55pSH9PyXzZ0tDQ8NiFF1646vDhw17qESDPetCzGXD0XYObwLYQ2FeVlZXNoRsvZ6Yfl+ztAd1Y2lNfXw/QXyHQ9TeTsm5g2Qp40BTZtWvX+aNHj15AGvv8rJN+BneYNPrm/fv3/2LixImbqZuy6ZLBve7atWwDXNPYNAzT0qVLS2fNmvUA7f+4gS4eAbxynRIwHN8ozDseFQZHTW9l4m/3BBa/8nnDfT97vKqBKmPQs2ZpMVsARz8ZbvPRo0ev6d+//yNkjiRlD0hvqUh3ecsrpwtD2+GEdcMfELUNbWLewNniBaqU7XNAnvGgZwPg6KMJx44dOybSstYf1coISSOKsy4tiJLa8ySPX6wvrxN3TLpX7KJaoM1xZDTkmfynPaixJ02aVEBX+PdOmDBhg4K754D2tqTFKKacPVhsOPEnce+kSQK/IjMdYChjFWWmdozhNq1evXrYtGnTltKNmq/3doLypXyyNLgsP49XfPhOpfjZzCcEbKGMtc0zEXBeITFXVFR8g5YAn1O2toxW9+FUAI5ewDY/UC9+PvrXAjeJ2DbH2nnGuEwyUYJae/r06bba2toH6O7a6wrujGGlS0eMBjFw1ADxetMT4oHpZ2lbi3GtlFEmS6Zo8CDca9asGT516tTltClK3WLvglRsEanS4HJvXF6x/v0vxHXfWigOUXzGmCyZADj6oK2S7N69ewrdtFlJWnuALDwVjk8C6QAcPSSTpa6iTvxwwm/EejrNiFWWdJsoDLeZdv3NHDNmzGoFd3wwZ1JuMlkGjBsoVlf9TsykfmGFBYorrUo0nYCjbQjAQjv/rhs+fPjLFLbToVwWS4Boto/sL14++ri4joZhoQNznDbO0tVwEG66K/nLIUOGPEW32/GLVy43JGAeUiKeqvsv8UsaTlohTwfgGtz0EIKV9iE/RjsAH1IPIuQG1SGjCAhDWaF4qHmBeGzwYIG3EaRFk6cacA3uGTNm2OnFNs/16dPn1hChqJOck0CJTdxaeY94bsZkzfxMOeSpvADQ4B44cGAB3cBZRg/8fi/nZjNDBpSuVZRow3d4xGsjHxT/VlsrXJSPlxGjFUlIWqo0uAY39dhaXl4+X8GdkLnLqkrsFvE90uTzwQAdKdPkqQCc4bZUV1fPpXeQ3JhVM6M6mzAJ9LGJG+v/IOZShSm78Ew24DCB0Ibl0KFD19Ozkr9JmLRURVkpgdIi8Zvqx8X1YIIOsJFUMzmZgKPj+FNkoQvK79CqyQIKK6ckIE4pEQsqHxbfIVGwJk8a5MkCnOE2b9++fRrdfl9CS4GAXTklATwiYRpVJp7bfb+YRuJI6h3PZAAOuFGv+a233hpPb5NaSeHkPGJCFSuXnRIgSGwTBomVb/2nGE8jAORgJuGaPFmAm+iB4KIpU6Ysp70lJdk5BarXyZaA0ShKLh4tls+6QBRRW/gLn3DAE10hfjDoaMHx48ef6Nu37w0UVi7FEsjEdfBoImh2iMX97hR3Up6Er5EnUoPjx4L6LHv27PmugjvalKo0WQJ97eKGLx8U3wU7dIChhCneRAHOcJtXrVo1dtSoUQvlAaiwkkB3EqAngxauuk2MpXwJtccTCbj5kksuKaLHzZYpu7u76VTpegnAHr9svFh2yQTNHgfkCdHiiagEPxLN7qbnKOeXlJSoO5X62UvxebbZ4LJ4yB5/muzx2RSXEHu8txo8aJps2bLlYtodqOCWZ0uF45YA7T68cfu94mIqmBBTpbeAa9qbzJIiWu+eTy9Wj3tAqoCSgCwBIETr4/OnjwsuHfaK0d4UZu1tef7552+nF/OMkzuqwkoCPZWA1SzGvXS9uI3K93pVpaeAM9zmF154YdSgQYPwaJJySgIJk8DgvmL2C/8hRlGFvTJVegM4Liwt9HTO78k0KUzYyFRFSgIkATJVCq88S/yOgr3akNUTwFl7W3bu3IlvTH5LzYiSQDIk0KdAXEEbsi7vhBysgr24XE8ARxnzlVdeWUw3dB6LqzWVWUkgTgmMHSgeu3Ky9oFeNlXiqiFewFl7m5988slb6XPXI+NqTWVWEohTAhaTGPnMLIGH0xnwuLR4vIAjv4k+gV1Cr3u4Kc6+quxKAj2SwIA+4qbrpwjsSsV1X1zMxpOZtbdl3rx5P6cX0Zf1qLeqkJJAnBIwmUTZ/TPFz6lY3MuG8QCOvGbc1DnllFNujrOPKruSQK8kQG/Kuple0Yx942yqxFRfrICz9jY/88wz15DtfUpMtatMSgIJkoDZJE5Z9mNxDVXHgMdki8cKOPKZ6Fs59lNPPRV3mJRTEki5BIb0FbdNOj2+N2TFAjhrb8vKlSuvIu09IuUjUw0qCZAErBYx4i/XiasoGLMtHgvgmvYeOnSolY47lKSVBNIpgWH9xR0lJcG7m93y210GaG8c5ldfffVfaEPVmHQOTrWtJEAbsca8f7O4BEzSwXxGFEwsgGPt0Ux3LX+ktsNGlKNKSJEEsJ121CDxIzBJB9gE5BFdd4Aj3XT11VfTM8R9Z0SsRSUoCaRQAn0LxIyrvyb6UpMAPCrD0RLxy0C6ee7cud+m5yzV50VIGMqlXwL0/Kb9nsvEt6kn3S4ZRgMcaRrg9P2cHyjzJP0Tq3rQIQGYKSP6iR/QGQMekeNICdDeOExPPPHEiMLCwq91VK3+VxLIDAkUWcXXnrhaYMma7XDw2sVFA1zT3jNnzryKtHekfF0qVBFKAqmQABFpnDUxRIvHDbi2ekKPo30/FR1WbSgJxCuBwcVBwFmLd6kinGbGLwHxphUrVoynte+xXUqpiIyWQKBoWEb3L1GdozXxsS/9u/Z2Wl5N6aLFYaTrXRDwr371q9PUxaVePJl/3nzef4uaN28S7hNHunQ2EOgSRa/r1rkuEXild1enr6unecJVHktd9OlwaOJp1LPddEApg92QotEANw8YMGCqApwktmen8K9cIURTI8kv810BdXGI72JR73LR9+ND5jvzOx9nD80u11QhVj1DxRjwkBr0gOMXoGnwoqIiM72p6sKQ3Hl64nv0fhGoPZ5Vo8ff7P5+v2jw+Eil5S7kfQKBC8FqW1sbAx6ixRGpd4gzLVu2bKLJZCrVJ+bjebbBzXNkoTsipfRQo0HTWRybWz7BWvqHkYMn0qjYDg8ZoB5w1uCmM8888yJlnoTIKitPGHIj3R3hyc0lHwCPLbRdRB4A56EF5yoc4Igzkf09RQEelFNWBwB5f3okJhfnE2MqNZumgFk6wC4gDzoZcKbfRLfmrWR/Tw7mUoGsl0Ao5DzVueEXmUyThxcW8heUeVDanMmAIwLnxoULF55Nv4w+Wg71X85IgCE3AoEccjScPr8ZderZNCSNX3lo8ioKk28aNmzYhFz8cyYPPF/DHZAbRKPXmzNrK6B6qM0ygbxP6WCOtaUjWYNzgpH2fo/JVwDyYdxmUuH9zWZN3eXKePuYjGAWPDPH2tD0GhwZTLR7cIzS4Jp8cvY/QN6PIG/KAU0OVouMxtPBLh0MuTZ3rMFBPRzOjQT4aO1M/ZfTEmDIc8Emt5s0wDV+OydNY5oBR5ym2ktLS802m21kTs+sGlxQAoC8r4nMFZp9DQAGIct8m9EwstRuh0XCw9DGqAfceP/992MrGrYzKJcnEjgJOdjIUhcQBbcPHQx2wXRwIGyDM/XGs846S22PzdI57k23AXkJmbAnfNm5dwUAn1mkbe3+ohNwRAVYgwcBHzhwoLrA7A0pWVxWg5xe5Wo8qQCzZjQAuNRs7rKSwhocAwHsRrvdPhwnyuWnBAB5H9LkLZomzy4ZFJmNYFfjmHuu1+AGevdgMScqPz8loEGuafLsGr/ZYAC7bI3A1x6751EgwkhbZIvVGjiLJH99QF5Mmrw1SzQ5mKVFcAAOpa3BjdnjkyD1BDheMq6ckoDQNDntQsQSYjY4ghzsBllGWLbBMQaDAhxiUI4lYCLNWEzmiqbJM/zBIKvByIBz9zUNzica+QpwFofyWQIMObGe0c4kAgx4sKeswYMRCvCMnsO0dY4hb/P5M/YZT7NJ0+AsI41pXkVBJCKUicLiUX4XCQDyIhNWyYP6sEuedEZE0+DcLwU4S0L5YSXAkLdrmjxslrRFGmOxwal3bLakraOq4cyWACAv1DR5ZvWTVlHArmaJcM/YRAn+zfF6ve2cqHwlgUgSYMi7rDNTASYs1b7PH5DZ1Zjm/gXH4fP52oInKqAkEEUCgJz2YWeMRU6Xv3p2NZWO1c3gCqfS4FFmVCV1kQBD7qS3aKX7LXE+v1/W4BrXbKJwxwNKg7MolB+rBAC5jd69Ql5anS8goMGDyhqdkS8otQQFeFrnKGsb1zQ5Qa5p8jSNwm8ImihByMNq8EC6/9akSUCq2d5JAK+H0zR576rpUWkwSyuXETW4Zq9QzQFlg/dIvqpQpwQYche9vDvVb7X1BgRs8CDL6JKswbUEAry1s6/KUxLokQQAeQFtQUz1HU96FzrYZcC1vss2uBbhcDgaoO7VnvAeza0q1CmBDsiFcPlTIxJQ7aTXoetbYw3O1PsbGxv/oc+kzpUEeiKBk5q8J6XjL9Pk9YBd/KSYZ81EwQmcFllRUbFfXWR2CET933sJAHKrZq4k9w4nelrldOwnLwg3wqzBka4lvPbaawpwSEO5hEkgCHkS18kB72v1zXrAg+vgTL3/7bffbnG5XLVms3lgwkaoKsp7CQByC0nBo+nRxIvD7ffXrjve1EI1dzFR0FoQcGRobW2tUmYKxKJcIiWgQU6gJ1qRg9U2X6CK+gq4wwKOcQByLUN7e/s/EKGckkCiJQDI6fUOCd9x2O7zgVkZcK3rbIPLGtyHlRSlwRM9tao+loAMOcf1xge8TT4vAPfREaLB5XXwIOTHjh3b35sGVVklge4kAMhhqngTsC0E9dR6fGA2BG70IZwG97/xxhs7aNMVgFdOSSBpEsDNxA5zpXdWuY/MjVW1zTuoowA8BHIGHIMA0Ej00Udga5ubmyuVmQKxKJdMCQByE/ENfd6Tf6C2xR+ofPFITS31UzZRtG7rAWfIvfX19ZsU4MmcWlU3SyAIeQ8UOYCt93g3keelI0R7o34ZcJxrGpx8X2Vl5UYFOESiXCokAMgBI3lxHTDkqxyujVQU2ps1eLDLMuD4MbAG9y1ZsmSTn1wwpwooCSRZAgx5PM3Qg3L+JTX10OAMN3OsVRMOcO2XsGrVqkayw8uVFo9H3CpvbyXAkMNa6e7AQ6DNXl/5W8fqGyk7a/CIgKNvrMFhz3hpufBTBTjEolwqJQDIAXd3DrDWuj34+KvGK/lsgweLyhockQw4fg1essM3KMCDslKBFEqAIY+mxdGdynbPBvIAuGyiIElz8o0eRLB6xy/Bu2DBgk8vvfRSZ1FRkU3Lnaf/VRaVCM/xmpwffSRlBijCuUjxyBsxLUJCpMfbkB39AvD6/jn8fufjh46wBmftHdJCOMCRUbNnPvnkk5bq6uoPRo8efbmRnphOhGvbWiGO/c9fhaeuORHVpaQOt+8rwlmCb7uHyC6k7UgpUctEKBStTEijnSf6iZfzRGhCyxJvO3K96Q7T42mi2nnig21N5dhByPY3Qx7snh5wJEAmmgYn37Nt27a/nnbaaQkDfP+dTwp3dT3aySrn9HtEu9+r2YYQUCw2IgbIeRk0lOO4cOmIk12q88ttI8x9jdR/fX79eXfl9en68pHG7w34xW5nzV8pv4cOeQ08pIpwahltMuDeW2+99SPaPlsfTUuE1NjNSTbCjSEVGS2i0NihD2KFG+U4L3w5jDQ4OY7zyL6cRw7LeeSwnEcOy3nksJwHYb1DXjguw2EtMob/uiuvT+d22Ne3h3iw6Az46he37PyITiNeYKJsNMCh9j20s9Bx+PDhvyUKcDSarQ6Q2wnyaNf4nMa3nTFWjsuEcXNfYu1fsvP3RCbQwLU+598a3W4HBaHBw15gou5wgCMeGpy1uGft2rVv0OskEJ/3DpDbjCYNWoZE9iEghkIOy3nSGZb7JIcj9UnOI4cTlT9SPdHiAeZ2Z93r5MnmCaK7uEiA40eCXwWo9sydO3dXU1PTl0qLd8iPIe84U/+nUgJgsC3g+XJJ8+7d1C4DDlbBbBcX7iITmZCZIUclbloTX1VWVnYnfcMH6XnvADmcKwDZKpcqCUBN13jbVpHnpoPNE+a1SzciaXBkRF1BLb58+fI36GFkVKhcpwQ0Td7lS4xKPMmUgFv4PG+3HXmD2pC1d1jzBP2IBjh+FSgIM8W9dOnS6qqqqjfV/iuShuSwsmJTkEsSSV4Qa9+1Pseb77ZWVVMr0OBgE4yC1bAuVsA1M+XFF19cTFo84q8lbAt5EKkgT80kuwMB/7q2I4upNTZPegU4eh2ixefPn1958ODBd5QW7zqhDHm0q3+V1nMJkPIW9f72d149UVHZCXi3cGOWomlwpENbB80UCrteeeWVZz0eT8Q/CSiUr64DciwhnnQcjnbjArk5PV6fy3KL+va6q6+35fX1d1dfd+n6+vjcL/yBjx3Vz1J5Fx2yeRLVougOcPQHFeBiUzNTfvvb3+4lLf53pcUhmq4ON4IKDB2QY3Lg2JfDPHFyHMLxOq67p/X1try+v93V1126vj6cd9jezr+vaCrfS6dsnoDJqHCjbCyAQ1sHAaewa82aNc+QFkd55cJIQA85w5cKH91hiORwKtpOVhs+4nij89gzNB7W3oAPTHZrScQKOGtx/Hpcc+bM2X706NENSouTNCI4QG4lTZ5qx3AzbGif41Ldl0S0B+1d73dtWNy4ezvVx4Cz9k4I4Ogna3EY9oDcuXr16oVqRQWiiexOavKTiOEyC44vtzisRXbGcxznicfnsrHWp8/P5SL5+v531zd9/fry3aV7aOVkk+P4QsrnpAPsgcGYtDfli8lEQT4ADi3OgLtmz569jd4l/rraowLxRHY2TZPjY6kd/5CTJ1kOR0qPXHP4FK471vr0+blcJF/uc/gehMbq69eXj5buoy2xR31trz/duGMblWPtHdPqCfciFhOF87IW1y420eBdd921gB5MblR7VFhE4X2GPHyqig0ngY49J97GxU27FlA6wx2z7c11xgs42+Jo0Pnee+/Vbtq06U/qgpPFGdkH5FhdUS42CeD5qb2exj997qzFG6tgnoC5mG1vbiUewFFG1uIa5LNmzXqNHmvbri44WaSR/QLaZstLiJyLrXP4sRwox2XkMJeV4+Qwpyfal9uQw5HakfPIYTk/tHej37X9vuMbX6M8DHfc2hv19wRw1uLaxSY6sGLFikecTifegYg6lYsiAUCO1RWeUGRFOFbHeSOV7y491nZizRdve93lB0Nu+qD8O22HH6E+AG6+uIxbe2MM8QKOMgw4flGaFn/ooYd27927dyW9kRbpynUjgSDkeP+HOkJkEKBfwCF/68oXmvdgv7dee4O9uFxPAEcDDDlrcccdd9yxqK6urlqZKrHJH5BbeqRfYqs/G3NhzftEwF39ZNPORdR/PI6m195xD6ungLMtzsuGzs8++6z+pZdeuos+f0JLl8pUiWUmGHL82c73A69hcwm/5/3WQ3eVOxrw2gWGO651b73cewo46mHIg1qclg23EOiL6AaQvh11HkECgNysNDltdPKLfe6GRU837d5CotJr7x5rzN4CDlMlqMUp7Lz88suXHzhw4CN1AygC0WGi8x1y3NCp8To++lXN+uVgqPPgu5ZgLC2AY6rQOA6+4MQvr/3OO++8j+zxGmWPkzRidJq5YuiNvomxoQzLBru72e+pWdS46z7qWjsdYAgmAJhivijYM5cIiaITWD7BLw6dc9ANoGMvv/zy3WSP0zeGevzjo6ryy2H50EKQR7pNnmvx0MvugN/7vuPw3Vucx47RbGv8kA+WeFmwVxAkAnAQjM6wqaJ1kl41sfnTTz99Cva4gjz2OQLk+DBTrjswAbt7r6fhqacbdm6m8cpw9+rCUpZdIgBHfQx5iKkyY8aMJbQh6123Gz9I5WKVwElNnrurK16C+4i39d05NeuXkFz0pgkUZkL+9CcKcMwdQw6acaGATrfPnDnz13TR+ZmCnKQRh4OpYs5Rm9yjXVS2f3ZX3YZfk0g0TsjnZcGEwQ1xJxpw2R7XIKevRJy49tprZ9MDEuVqUxZEHrtjyHNpjRwrJvU+R/nDjZtn13scJ0gaAFyGGwwlRHtD0snY3obOyR00EOTe/fv3r582bdr0pmXv9MVXbpWLTQImklWHQGWRxlY203IB7kaf69CC5p037XDU4osCbXQw4DBvE6q9Mf5kAI564UJAJ1vc1dLSsnFUZctltKOuUEHeIaRY/gfkcBBotq6k+KnzJwKe+mUnym9c13roIA1FD3dCNTfkBZcswGV1Ewxv3bq1rcBk+Xycpd836c+vVUHeMQmx/M+yCgozlkIZkoe2mYrWgKf19ROVt/y55cty6pYebmhuAJ5wlyzA0VGeC/a1zm9z1jaVme17hluKL6HVAgtPXMJHloMVsqxCBJrh4+yA292+tv3Ifz7btAu34WW4E7beHUkMyQRcbhNzEpyXTY5jx+kJly2jrSXTSZPbeeLkAiocXgIsq6Aww2fLiFjY3Cf8nsbX2/bf9mzjLqx1M9xY80463BBCqgEPgr7VWdvQbvCuH28tnUo2eR+eOHRKuegSCMqKTHOY55l44F0mDQHn0eXNX9z8yomKPTSiVjoAOMONmzlJsbup3qBLFeBokJVOEPJyV2PLUW/738+2DbjAZjCV8cVUsHcqEFECgDwoyIi50pOAde46n6NiYePuW9a2HfgH9QJgpxxujD6VgKM9OJ4XzT/gOeHY7W5Yd65t0CS70TRE24nRuWrQkV39H0kCDHmmrK1gZnH7/ZjXsfWRhs23b3HUHKW+682SlGhullmqAQ+Bmzqhndd6He5PHDUfTC48ZXShwXyagpynp3ufzRUIMp0OuwLpWUq6/d72wd21G+fsdzfVUX8YbtzIwQ7BlMINeaQacLQJ1wX0Fr/b9zfnwQ/PKxhUUGKyTjIJo4Enr6OI+j+SBGQ5YcU81Qfgdga8gQpP0/O/qP/4d41eB77yK8ONC8qUww15pQtwtM2Q84VGwEsbyN9srdo60FRYPsRin2wxmOzYS4AHc5WLLoGT5kr0fIlMxY5AvL+k2e9ufK/98Lz7aje9SnPIF5PyHcq0wI2xphNwtM+Qww+CvsFRfaTa2/beuILSM+0G0ynKZIGounephJxNkhpf+7aFjTtvp5WSHdRDaG3W3DBJ5KVAzHHKXaYADrj50KCv8rS0rXFUvXtOwSBTX5P1K8pkiY0NNleSSRNu3sAkKfc0L7+j9sMH97ua8fYpaGyGW795KrbOJyFXugHHkHgu4DPkmjanP3e+Na1V2waa7buHmAsn0/ZRu7YXQ5ksUVE4adIlducKcU0mCW7euBvWOo7c/UDtxr90miQMN9a4+WIyabffow5el5gJgKNLMuQy6Fp4g+PY0cNksoyylgymz4SM7nioS9nmurkMOT0JeUh0j05ga/toiuj78OKQr/W9RY3b57x64stdVBlrbACuv4EDJZV2l2lXb+gPrivxw8OXVgvosNFhp6MQ4TvKzvnni+yn/rLUaB2BJ1/4TzKlKRdGArCVe+PY1m70uw9+7Kz+wx/rt26g+mCCMNS8BMjbXbW/vr1pM5FlMw1wjA19wgHI8SVmKx2AnEG39zfbiu8vu+CasdZ+19HHWAvM2ESqzBYSUXgHDRwv5rxC0ub3uCrI1n6w/tMXGr1OrJBAU+MA2GxrY5WE93LH2xQVTZ7LFBNFP0IWEnwc0Aps0/mcfq/3rbYDO+r9zrUjLMVDaePWCGW26EV48px//Kw5ovl4wxSbI/Ty+Y+fa97zq0WNO9aRzOXlP2hvGW7MT0aYJCdH3RHCWDPZoX9ssrA2Z40Os0XT7HMGnP/1C2yDbulrtI7E64nx7lae1EweXKr7Bq0cybGd7SI7m9a1D3zmqln0WN3nH1J+1tSstbH0xzduWOlErjhSgymKz3TAWQyAnEGHbQ7I2T7XIDebzba7+p1z6STbgGv7GwvGKNBZdNF9GWx6J/eXO5x1z/++aetaejMZg80+r2vLtnZGam15xNkCOPrM2hygQ5sz6ACcYYdf8Kuy8y86zz7g2jKj7SwFOkkkjJPBJlNv9xZH3fOP12/+iLICZBwAm31obIDNdyQBdsZqbepb0GUT4Nxp9BnXDgw6tDlrdA1wOtf828rO+afJtkHXDTLZz7XiNQxUBIXz1XwB1KASa9n0Rilx3Of4fJPz+PKF9Vs/o2gGW/ZlcwRQ84UkBbPDZSPgkKzGKfmy2QKNzkuLMujWG0rPmnRhwZBvDzbbp9sN5kLAni8XpQy1n9AG1I6At51edPnBRlf1G4sbduP2OiCWoUYY2poP2c7OCq1NfQ+6bAWcB4D+A3IGnU0XBp01u6blh5qLi27od8b0Mdb+3xxosp9PoJvwch3Anmvr6Vi/BtRegprA9tX6HJu/dDe+vbhp7wf0RincoGHtDKAZcoaal/2gtbPGHKG+dnHZDjgPSA86TBi20dmEYV/T9FMKTx00q3j0ZSOsfb5ZYrCO0UyYLNbssqbuhBpfS/jyoLvl7f9r3f/O+vajx0kmDDDDLftIg7ZmjZ3VYNM4NJcrgMvjgTbHuAA5NDprdQDO0Mu++Yf9xo2dXDB4Kmn1c/uZCibShWkBPi+CR+gy1ZSRgcbmJzxJQ0t8riafaxdp6883uWo+Xtm0r4LGDG0MeAFzOB/prLFhguQE2DQOzeUa4PK4WKsDdD4Ath54TaNTvJbWz2wr+FHfsRMnWErPG2iyndvfVHAGwW7BBSqA7/jX0QwLL1kXrYAYjg1f+LhMBNC4UCSoPfSmqL21Pufn5Z6GLS83V+xq8jphcjDUAJoPBpzTWFuzxu6ongrkkuM5yqUxyWPB+Bh0va0ua3eGnOPYNw21FRX+oHDcV06zlpxdQvtfCg2mEYVGy1CrMNpZw7Mvwy93AmG9oBlafT6GGPYzQGbfLfyOdr/nSHvAd5B28x2scp/Y+Wr7vu1HnG24qwhIWROzz2DD5zj4DLRsX0fqDmXPbqeXe3aPJnrvGXT2WavLpgyDrfflvNoP5eLiYYMmWctGDjEVj+hrtowsMliG01cayugppEK6k2qnbWCFlNGMxhh81vRsXkAbgywizUuvWWinW+QOT8DX7vL76tsCnkPNXs+Bal/rwR3u+gPrWg/DhmYoZe0rwxsuLOdl84P96BLLgdR8Apynq4O5DqWKMOAFtLIPwBlqhp3P9Xk14DvrCKmbzB3zSGtx0RBjob2fuaCoj8GKHZGiJeBub/K62qr97Y4D7tY2MisAZofyPukzzPBZ68o+wgw0Q4xzOQ+X1ddN2fLD5SPg8syGAEkJMqx6kGXA9WlcDvUhzPWiLTmMc9kxeIjjMGtXBlwPLEPLUPM552ef62NfbjdvwhC+ch0SYFkwkPD14PI5QwzokY99jg9XF1rheEAHx/DpzwEp4gCx7DO87CNNDuMcBxz7HWd5+j8LPE+HH3XYLBv40Q4Zan0+NCDXIzfIAMo+wvqDYdfHy+eoF+fK6STAwtdFq9MIEmB5McjIFi1OTo9QZQiYDCnDizLR4iLVqeI7JcCTowTSOwno5ag/R+36OAZXblkfpz+X86pwDBL4fwN/IZwMBwH5AAAAAElFTkSuQmCC', // @suppress longLineCheck + + yellow: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALcAAAC4CAYAAAChOH1KAAAlaElEQVR4Ae2dCZhUxbXHTy+zL8ywDDsSVhEVJQoCkoSIIr4kvohLxO2ZfC8an0mQrCQm+uJ7qHkv5hE/xSQaNokBogkxigaUuLDIpsiOMA4MOwyz7zPd7/yLOZfqnu7p7umeXut83+2qW7du3apTv3v63Lr31rWRkUhowOZViPc6Nutpbq/8WPVO8173sYtJ6kgDusI7yme2nYdTdIZQj0NH1vrgwYNtc+bMyb344otzCgsL87KysnLT0tKym5ub6+rr62vKy8urd+7cWfv444/XlJSUAGSB2VfoKw3HM9KBBqQzOsiSspsEXgmhCMTtDGT2TTfdNDwvL28kQzvC6XSOcDgcQ2w2Wzfenme323M5nuN2uwPql/O5OF+dy+Wq4X2rOV7Z2tpa3NLSsp9Pgv3V1dX7XnnllU/4RKnj7S5edNARl4WjRnQNBFS+njnJ4wKxHtp37NgxpG/fvlPY6l7CAI/kZTgv/YMBN1L64hPAzcAf5eUTXvax9d9x/PjxtZdcckkxH0OAF8gljNThE7acVIdbQLZzDyJuX7du3YChQ4dOycnJ+QJb5M+zFR4Qr73L1v4IW/h3amtr/3nw4MG1kyZNOsJ1FdglBOwpKakItwfQTz31VN4dd9wxnd2LL7J1/hzDPCxRSWDYD7BVf5fdmbeXLl26avbs2dXcFsCdkqCnCtwCNEL7gAEDnBs2bPh8QUHBnenp6TdyWi4vySY1TU1NKysqKl6cMGHCO0eOHGnhBuqQJ71FT3a4FczcqQgdu3btGtWvX787MzMzv8YWun+y0eyvPWzRjzY0NPzp2LFjL44ePXoP52vlRbfo/nZN6PRkhdvyoX/7299245GNe9iHvoMvBC9P6N6KQOX5gvRD9tGX8gjMovvuu6+SixRrjjCpJJngRluwAGz78uXLu0+dOvXbDPW3eL2QFyOeGihnyOevWbPm6VtvvfUsbwLcAnpSuCzJALcH1KtXr+4zduzYWbm5uf/OnZWMvrQnouGv1bD8ftu2bf937bXXnmgDPCkgT3S4lZXmDrGvX79+0EUXXTSbRz3u5fXM8Ps85Upo4FGWBbt3735q4sSJh7n1YskRJqQkKtyoN8B2vPzyyz2uueaaX2RnZ9/NN1bSE7IX4qjSfMOoqa6ubvFbb7318xkzZpRx1XDxKZY8jmoauCqJBjfqi8WB4bzNmzf/W/fu3R/j9R6Bm5o6OWyuErK1fMiKwuhfJ8VNZWUVtT/77MTHFrYNI8oIS8L444kEt7ggju3bt182bNiweXwHcXwnuy5pdwPYzrofMdiR8SaaW9wffFLc+N3RE/7xEStNrHhkCu/iXkgEuFFHBfb8+fMLb7vttkf4YvGb7II4u1g3CVm8vXEpORtfiGzdbbaWmpqW3724ou4/v/WDj8u5cMAd965KvMMt1tp56NCh24qKip7gmy99IttzyVWao3ERYekKcbnpxKmy5h/3HbVpGZcvdzzj1ooDnngUnHQOLI8++mhBZWXlC3369FlowI5tV9lt1KdPz7SFdaUTXnj00REF0kccxqWRjMdKWWBv2rTpUn7YfwnfWRwZ225NnKN3peXWtdDion0799TedfkXPvqY0+GLywWnni2m8Xiz3KgPLLbz6NGj3xgzZsw7BuyY8uH34E47jRxzUc47J/eN/wb6ixf0W1zxFC+VEWvtnDt3biE/ybagZ8+ez7CysngxEqcasNkoq6i785m6w1ctmDt3GB5xEMjjwiOIh0qgDjjJMG49hp9ae5Gt9fA47c+4r1a03BJvRbS43J/s3FN3R5ubIhebMR0Tj7XlFoudtm/fvmsuvfTSNQZsb2wSY91ptw0fMzrnrYObr7iGa5zGC9yUmBrPWMItYGOY7xZ+W/wvrIw8XowkqAa4Q/M+MzjjL0d2jL+FmxBzFyVWcOO4OLPT+AH6b/ELuAs5bp4LYSUkujDg6f37Ohee3ncVHjUWCx4TzmJxUAE7/eTJk4/06NHjKb7bGIt6JDpH8Vt/N9l7dnc8dfaTcY9wJWG0YjKSEm2oFNh80ZhRVlb2NL/D+KP47SFTs3A1UFiQ9qOakglPjx7dKyMWgEcTbgX2+PHjs3j6hCX8fMjXw1We2T/+NZCTY//65jeGLRk/vjuGdaNqwaN1NavA5salnz179jl+9evO+O+WxKxhrIYCA2mrtq71xdwLNt7P+Zp4kacLA+0W1vZoWG4BO4197McM2GH1V8LunJPtuLP84FWPcQOidpHZ1XCjfCxppaWl32Ef+6GE7R1T8bA1UJDveOjUnvHfAQ+8CBthl+uvgK6EGy6PAru4uPj23r17z/VXCZOeOhro1cs5t3T7uNu5xQJ4l7nGXQU3KqzGsfmF0+v79+8/P5oTR6YOKgnYUjfZ+vdLm7/vg7HXtwEOTroE8K6AWyy2kx9ZHTdkyJAlbY1IwJ4wVe4KDTAgacMHZy3Z9vbl47h83MkEhxEHvCvgRpnOefPmFfF49lLMU83rRowGPDRgs1POpRdlLZ33xJAi3iCAe+QJdyXSZ4sCmyuVwY+truA5RKaFW0Gzf2gaiNehQH+tqKt3vZkzaAOeRWnkRZ4m9Jc9pPRIWm6cKMrPPnz48HcN2CH1Q8pmzs6yTzux+8rvsgJkiDBiBjdScAvY8LOv4hd58UyBEaOBoDRQ1DPtEfa/r+LMEX2SMFJwoxzHE0880Yv97AXsZ6OSRowGgtIAeLl0VNaCJx4d0ot3wL9/RLiMxF8AKgKY4WcvY3dkOseNxEgDieZz62qqq29dlTNo422cFhH/O9wzBCeHgptfOHjAgK13lYmHqoHsLMf0ozvHPcD7yehJWMY3HLgtsBcvXnwB34F8ONTGmPxGA94a6FuU9vDiZy6+gNPDBjxcuNXoyA033PA4+01mLmzvnjLrIWuA36jP/dcb8h7nHcMePeks3JbV3rp167X8sVF8NMmI0UBENJCXa7/xo3fGXsuFhWW9Ows39nNed911uRdeeOEvI9IiU4jRgKaBi0Zk/vK663rDGxDAta3BRTsDt2W1n3/++Vk8jfDQ4A5lchkNBK+BNKdt6OJfD5rFewjcIV9chgq3BTZ/UGkY36wxz2cH318mZ4ga4Js7Dy1fMHpYZwHvDNzqIpI/1fEkX0Sab8+E2GEme/AasNltmdO/kP8k79Gpi8tQ4Las9rvvvjuBXxe7LvhqmpxGA53TQE6O7bp1r4+RW/PgNWj3JFS4ldXmW+zfY6vdudqavYwGQtAAOONb89/nXUK23sHCbVnt119//TKelmFqCPUzWY0GwtJAbq596j9eueQyLiSki8tQ4IbVdl555ZWzOTRmO6zuMjuHpgGbbfxlOeAOcIPDoPgLBm7LavM3H0fl5+d/KbSKmdxGA+FrID/P/qWXXxw1iksK2noHC7ey2pMnT8bQXzD7hN8aU4LRgIcGbPYpV3UDf0Fb70CgitV2LFq0aAhb7RkexzMrRgNR1ADPezJj0fyLhvAhYWzBbofuSbBwO6dOnfogX7miUCNGAzHRAA+cOP5lSt6DfPCgXJNg4HawO5LDs0XdFJMWmYMaDWgaKChw3DR5ck/MqBDwwrIjuMUlwTQN0/lzHvjuoBGjgZhqwG6ngmfnDsTbXgGtd0dwY5u6kBwwYMCt5qZNTPvUHLxNA+BwYP/0W3lVLiz9MuxvA6w2FsecOXN68fPaX2wr2wRGAzHXQF6O44tzZlsvEwur7erVEdzY5rz77rtv5s9S49anEaOBuNCA3W5L+/rt3W/mynTomnQEt3JJ+JvrmA3IiNFAXGmgX1FawC+m+YJbzLxjyZIlI/mNdtzTN2I0EFcayMqyXfbS7y4cyZWSURNw6yH+4MYOjokTJ95iLiQ99BX3K271Tx331Qy7guBy0vg8WG/FKoft4IbP4i3IpPztwsLCz3tvNOvxrYEW23iqKPs9VxKfnUlc4fncPSrvtUpYb2lygU+/frc33JZLcs899xSwS3KpxxFSdMVWv4dsle+Qzd0c9xqAGevm/AJVVBSTy+ViCDwhQQN8JLVLc1P7/bz39VXOuTye+/rK5zvNcz+U1ZG4XO5Lb5teWbBs1QHMUCXsWoV4w42yYLUd99133yQ2/dBVSoutbhc5997MmkscS4hOK2hxU1mlb7h9daj3f7r3uq99Yp3GJ67jnqktk5atopVcF3Dr0UnecKNNCu5+/fpNNv42m4PyN8jWdDTW/Rjy8TF22yPLTWeriFyWLQu5mLjeAbD26eaezMHfeQG3SLJaiwRdsK7g5icAJ+kbUjVuc+OziYkpPD0Cdc/nDk0EM9xJFedn2ybyrvizEnatknS4oQIsjlmzZvXMzs6+0MplIgmrAQHcwT0tHZxMYXaGe9QDX03vCW55kaap/vIFt33mzJlXt2VUmcxPYmsAgBfmsWkD4Nz9SbbYvnq1G7yC5Q7hRgYH35W82vjbiQ20d+11wL23JfI6OO1TSIBbXBMArsTbciu4eU4Sc1dSNJREoQKcZ9+DBY+U4F8AIv8GEleJUfrJyiTw2g5uGS0R2hXcPL79mSjVyxwmyhpwwkXJdVNFTeRGUQRwNEXiEkajedkZBF4FblUN/nHr5zDi9p/85Cd92NSzh2YkWTUAwAtgwcWkJXhD+UTKm3VLWh9uhmJYmiNwo5lY7Pw8yXDZaMLk1YAArkZRuOdhaRN5mXSxDdyCZ2FZrUgPKrj55s1QczEpKknuEIB347cRYcGFiEQMUf++3V1DubcEbtVx7Sw3v3UzLLm71LRO14AADqudyJKbaQO3ArdqjQ434naeB3CIsdyJ3M2h110Aj+QoSui16Pwe4DUnm4ZwCYphDhXcGC2Rcxahg0dKkMlIimkAgOdnu6mqzvdTg/Gujqx0G7jFiInFM0iHIME+atSo9MzMzAEqxfyknAbOAc4gMBWJdnGZke4eMOozmengmBcFuA63bdq0aYV4jDDletU02NIAAM/LOge3lZgYEceUMa2FXFWAbcEtKzaen4RHP42kugYE8M6Mg8uFqVh+6FLSfOlVtnU2v+yHcFCRG/xaPOt3KG29evUyN2989UAKpgHwXH4evKZee0A6SD0IsMgucQl9FaFvk7iEgfLr27vnucGvwK38E9lu42FAY7lFGyYkZcGz2YkFLgkg+TkOsdyqtjJaomjnZ7gN3AnQidGsosOhWXDrHZdo1iC4YwHgzEzfbglKsBm4g1NkquUSwGsb4neYEG5MTjp5WG6P0RIeBswxN3BSDd3g2gvAc/irox35wsGV1DW5UK/0DDemNlZeCI5ijQkikT91jY1GjAZ8asAX4AI7Qj3us4BOJOpl6nFfRaU77AI3Ntv00RK+gDBw+1KaSTuvgXOAu6mOZwqRuUcEOuSSuITn9+x8TC9L4hLqpTqdynIjCdbbc+6t1tbWFiQaMRroSAMAnF/MpXoA3lHGKG9rddk8+BWfG9WwNTU11fqaoSjKdTSHSwANAPCsjDYTGQf1xb9IYzPVclWU1UaVdLipoaEBG40YDQSlAR1wuYrzDlGQRZuPUmWb937+1r3L0/fnuQM9+NXhdhu4fWjfJHWoAQtwocwrtyQHC6vX7u1WvctDBkmrb7YBbstTErhVQl1dHb82asRoIDQNAHA8j+frIi+0ksLLzRe5wq/iGaMlQrq7oqLCw6yHdyizdypp4JwFd1MDzz4noyjRbD9OrMpaD8vtlqFAAO4uLy8X8qNZL3OsJNEAf6uGLfg5wKPdJMBdXuMCv4plHF/cElWX06dPV5vREqUK89NJDZwDPPouCv4tTpVTtV5tgVvRvnv37hoDt64eE++MBgB4Bs+hDGvqvaA8pIlIXPIhXdIkjx7KNskvIa4q9xyyA24Py40ViHvlypXVPNbNMzobMRoITwMW4F7FeMOJzZKmxwVa71DPg7hIczNVvba+SdwSJFszTgntbh4xKTHWW1RmwnA0AMDTYcG5kK5ccAXLIyUlfBiLY9Rb3BLEscFVW1tbghUjRgOR0IAFuOaKRKJc7zJqG2wlnObiRTwRBbfQjg0uHg781Fhu1oSRiGkAgKfxuJy3ixGpdVS0qtb9KQeKYQ4V02K5BXA3j5gUI7MRo4FIakAAj2SZelmnKuggr1scYxvg1hNaecTkoLHcUI2RSGtAAI+UxZZyUM89h1wwyviamcWzWG5sVyZ94cKFn/L3CxE3YjQQcQ0AcCfPjAMwIyVMq2vhasenXJ5iWMoVuIV2165duxp4xOSYZDCh0UCkNaADLtY3nLC+yXZs14EmfsPTuqAEz9ZoiQU3p7XW1NQY1wTaMdJlGsC7urDg4Qpc6JoGN/xtuCSw3MKyB9xi0l0nTpzYbPzucNVu9g+kAQE8lDFwlOnh0TDKJ8tsmznZ4pfjHpYb+yABGVq3bNmywbjdUImRrtYAAHeE4IML2HJC4OvIW/e3buB66pZbVbudz41MP/3pT3fziwvmNnxX96wpX2kAgHd2ZtnGFqr68QuO3eCWF59uCQ5iWe7q6uqms2fPbjWuCdRiJBoaEMBDORb4LKugLYwrvmGuw62KEcuNFQtujrccO3bsAwO30pH5iZIGBPBgR05QrWNltk0c4K33gHADcGRq2bBhw3rjd7MmjERVAwAccAcj8LfX7Wxdz3kFbvCLRYleDOIYnOEX9tWca93OnDmznmd+7aFypuiP48jjhMVIdDUQjNdQVecuK7iheSLXrJIXPO7KM6ko46wAl9fMOE0Rj0Q1YsJhC8O9mT+Vfb09Ub8EhFaFKc2taVReYRmDMEszu4eigY4Ad7HZPnyKMAQoVtvjYhLH0eHGusCNHVr27du3euDAgRGF21axm2zH1pLN3Yzjxb3YG89Qel02PzIM3bUXf9jjtSdf4i8def3s4veFW39l+StHHaODjaGW5zd/R8fw08pQy2ppddOuva2rGVPFKrdN4EYzlehuCRKwDuB5Pk/KGzlyZM/169e/z5/vi8gXFwC28x9fZrDh1ieOVNW7cBcscSqcAjWtbXRXf/l/K6/ed6rpDDcXr5fh9jtAtzrKl+XGRtDXzJa77siRI6tHjBhxUyRcE9vhV8lWe4SLTizJR3XZLtRiwMlIzDWAx/qOn6HV+04Rf1iQ4AKAV3Brgc1x6/Y74iIw71hwFjTxqMlKniBTtoUV2lyJS0c+f+GLJzc3EgcaYI+EthyilVwVAAVOhVmP2unj3LJBLLfyZe6///5NVVVVRzty7mXHZA8BeC6PJcF3M0tsdADbzF94OPq9P5OMb4NTsdweCPqDWwCHyW8uKSl5zcB9Tm95fDWSg8FSIzHRAC48SyvoNT64YpNDARvMeogvuJEBZh474axo5ikf/trM784bOacBATzYO2kmH1t5/quLxNLCCK/aSX8Fl7yI1Qav7cQf3DgLLL/7ySefLC4rK9turPd5/QHwbOODn1dIFGKw2uW1tP3/3qZiPpzub7ez2qhOMHDj7GjasWPHSy1qSBG7GYEGlAVnwI3/HR0dtLK53XWCXmLVC9hgE0Y4JLg5v9oBO8L8N82cOfM1nvah1FhvqOa85BoLfl4ZXRiD1a6sp9L7lil/G3CDS79goyr+LDe24Wyw/G5+9axh+/btf4jUsCAOkCwigEfCpzRl+PbN8ZDUzhP0h5oadbNG97d9Wm2wFQhuAVxZ729+85t/raysPGmsd/vTEoBn8dRhRiKvAWW1G+jk7OXqQlKstt9REqlBR3AjD8w+CgHcjUePHq3duXPnImO9WRs+xFhw31Y33H8jWO29J2jR0Qr1QSc8+QcewSX49CuB4IblRgHqopLDxm9/+9sr+E2dMmO9fesUY+DGgvvWTWdSYbVrmqjsxytpBfjjRS4mO/S3caxAcCOPWG8FOD9vUrVnz54XjfWGanyLAG5GUcIfRcFzJPtP0ov7jhPe6RWwA1pt9EwwcIv1Vn4379M4Z86cl9h6VxrrDRX6FgW4GQf3rZwgU2G1qxup8ud/V8N/YrXBYUCrjUMEAzfyifVWvvfGjRvLN2/e/LS5awnV+Bfc5MnCOHiE7s6lWjktTN22Unp6awmVs5aD9rWlR4KF29t6N8yYMWMFT96z07xnKar0HQLwTDOK4ls5HaTCHTlVTTvvWqR8bTyrLaMkQVltFB0s3MjrYb358yL1y5Ytm8vzm7iMewL1+BcB3PjgwfnguMPC85G4Xt5Gc3nShnrWbMhWG70RCtztrPfDDz/88f79+/9sLi79gy1bBHBZN6F/DeA2+4Ez9OdfvE4fc65OWW2UHgrcyC/WG38ROJsavv/97/+Gb8ufNdYb6ulY4H/DRUk13zmU9kKDlY109sd/pd9wFGDLhWRQIyTYXyRUuGG9cRAMC+Kg9e+9914ZX2D+mt0UXjUSSAMAPMP75b5AO6XQ9iama0sJ/XrjQSrjZotLAt7AHfgLWkKFGwUL4GrkhNfr+eJyJd+93Gbck+D0LoCHYtFSIS/uRJ6oom23v6BeIROwwVnIYKMnOgs33BPrriXHG+bNm/cIv45WY0ZPoNbAIoAHznk+By5IIXJhKnGV2JYuaXpe2R4o1PfR4/720/PocX/5O0rH6EhlA9U88096hPPp7gg4A28hWW0cqzNwYz/xvS3r/dxzzx1cvXr1L3j0hOfZCLkeKDPlBP43XBSAEcwCBQlEelz21dP0uGwPFOr76HF/++l59Li//P7S20ZHaO0++sXv31cfbvK22uAtZOks3DgQDijWG2da/V133fXm3r17l5ubO1BPcALA01PcB29mp4OnaVj+jcX0JmsNYMsIiVjt4JTplSscuGGeAbhYb1So7pZbbvnV8ePH9xr/20vTHaxaFpxNWyr41nob4WefrKG9dy6kX4EfXsQlAVfgq9NuQAS+SsKHPy829rt5gquWDydMmPCVjIyMdMzaKWI/8S5hMdJeA+r7MNyN6GwR0Zy/v/NIp+O4ckw9LsfR0/S4bA81xAx1VQ1U+8s19K1/7qVjXGYtL7Dc8oCUpg0cMTQJx3LjSDi4t3tSN3/+fPjfjxn/O7TOyGAXJQ2f0ODdsEAkPLfWtb9yLH/HD7Q9lNrhsqyBnY639tJjv39X+dlitQXssKw26hIu3ChDABf3BGdeHfvfq/jR2BVm/BsqCl4E8OD3SMycGM/ec4JW3LuYVnELADa4wb2TsN0RLkNJJOBGQTjLMBbpAfj06dOfLC4uft8ADhUFLwAcF5m6b5pMcVxAlpyl9298jp5krXiDDY7AU9gSKbhREVhwffSkjt+3rLn55pt/WFpa+rEZQQmtrwA3XJRkE4B9pJw+/trz9MPKOjVhvLc7Ao4iIpGGW/xv/L2o0ZMDBw5U8HyDD/HjscUteEDXSNAaEMCTxWrjgaiT1VT84Ev00IFTVMGKELDBiwz7RQzurrYNqqKHDh1q4tvzGyZfmHVNbsUHufz5byNBasDRZn7kvpiuOokj9LXgEJJHj/vKK/kk7Ex+7KOLlIUQdyBP19CJOa/Q/W/soSOchJERwB1RP5vLs6Qr4PZ55vHFZb2r1b1lXNHRa/muXKYB3OqDgBFfgOvg+CtAz6PHuyo/jqEvOA7WYbF5GrSKX71NDyzaQAc4SYb88O+O67SI+dlcliVdATcKB+CyyMFsH+w6WsnTAO+8pD9dx4CnGcBFNYFDAVwfBw+8V+xzAGyeKar+D+vpu798k7ZzjWCtxR3Rh/0iXtmugluvqECO0MbPD5zpmUs7RvamKQx4Rgp/S0rXUVBxAVwpkk1ivPviAPtsPVUv2Uizfvaq+jgToBarDbBhsdGcLpFowo0GqIas3kOnud0fjBlAk/nWc450Wpe0MMkKFV3BB9ddgHiLtzC27GOfmvcWPTj3DfqIuwFQY4ErolvshIab22KdnWiIasyGYqrkZwreu/ICmsivYBVIpyGzkY41IP92cpHZce7ob23icY/jVXToZ3+jB55fR59wDfCNSN1iR3xkxFcro2G55bhyhlqAf3yEanccp7WfG05j+XszRQAcf7VGAmsgHgHHyQawedqzXQ8up/949WNrVEQHO2J3IANpKZpwS10EbhV+eoYaV++lt68dRaPyM2mAAVzUFDgUwJEz1v436oBb6p+W0cZbX6CHNn1KpzkJUMPP1h+GYo/U+ifnaNdJtOHWwZZGus/UUMufPqS114+mfvkZNNxpLHjQPS6Ax9JFwRh2Hdtjnqzy9WnP0k9Ky9QNmpiCDQVGG27pNB1yxF31jeT63Xu07ooLqLx3N7oy3UFO6TjZyYS+NaAPqUbbgquhvkZq5FGwX13/ND3L/YgPnsLH9jXch76OmsQKbjRQQd0WWrAv30r7bXbaOLIPXZHppG7GTQmOBQ/AeZeuHj1B7/HEOXSikg4/vZZmzV5Ba/mwsNbiX8uoiNygQR9HVWIJtzQUjYaLYrkp6w5QOfvhb04aSn3YDx9m3BRRVcehAN7VFCk3hAfz9p6kN29fQD9Y+REd4poJ2GKx9TuPXV0ln4qJNdxotPeiQD9dTc3sprw39gI60zufxsFNkb9cny0xiUoDMtrUFTTBr1e30huo8e399D/TfkPPcj9V8oF1/1qeFRGLHbOeiTXcaLj0g1hvPXSv2EqfuG20bngRXc43fAod/H8rHRgzrcX5gS39tOlKjEI4IcDGmzPHKql43ts0i7/g+y6rQe44yoiIgC19GFNNwTWLF0FdcLJh4cf1Cd/p5Q9SqyW7ex7lLL2X7vjsILq3WyZ/vIBzWZ3ImYy01wDch3AFUOMZbJ5TpH7rYVrAs64uPVvtYan1N2hgrbGIwQr38GHtHw+WW28AlCKLnP0IW+ubyLX0A9pxqIzWXNib+vLk7oPlYtNArqvwfDwcvYgLUsu2eP9pemfOSvrBwyvpHe4HuCAyGgKwvZ/siwuwoYV4styojwieYsaCGT14dj1lxfl7YZYlz3j2drr6Xy6m2T3zqD+PqpAZNmTt+BGAiiVYgcWHC8L3H46+toOeenAZvc/7wuUAzAI01vVnRCLwP8ElRlDiFW40EXUTwAVyAG4tA3tR3oI76J4xA+mOvHTKgKtiIIfq2kswcANquCDVTdTIj0YsvXcpLSo9rcatYZ31RaDmU8Aa5Wp/0BinxDPcUA3qhwXuEwCHLw5LbgGO+MyraNCDk+nuEb1pOrsr6TyyYiBnxXiLP8ABNW6dswvStP8UrXrmPVr8x410mPfXgUYcUGOID1CLbx3CfwLvFUWJd7hFFagnrLhALq4KLjoF9IyvXkb9Zk+lmSOK6Cv8XfZMfl7cQC4a9BECatyIqW6gBob6b0+toT/+5SM1OQ5cDgEbcd0FAdRwQeIWaq6bkkSBG5VFXQVyWHFxVQC4QK7iU4ZTr4e/RLeN7kdfzcugXAU57xnOBRYfIykE1htv8yioG6lm1zH6y3/9nZat/UQ96CQgA2yJ+3JB4h5sdFYiwS1wCeDe/jjAFosOa57+2c9Q4X9/mW4Z3Zdm8J3OQszJJ3c7Uwl0AI0Fkw80sFPBU5iV7zpOL//0VVqx9VP1pTAALEAjrltq8asTwlpz3S1JRLil8gI5XBUs8MdlfFwgV8AX5lHW4zfSxCsG0vUDu9NEfnY8Xax5Ml+Awu0QK13bRE2lZ2n9llJ6g4f11pdXW4+h6hYacfjUcus8YVwQrnM7SWS40RjUXyAXn1wgB+ACucTTxw2mbj+cRlNH9aZp/QroYobcpmZ3QkFcUiJbdLHQ8Bnw0gC7Hu5jFbRzDz8Dwi/nrtlUom6VwzLLIhYa6zrUsNJiqRPCBeH6tpNEh1sa5AtyfXQFcAN6gVydAHdNoAF3j6PpQ3rQlIIcGoxRFoCubg5x5niHXYcZz3wAaIx6VNRSSXEZrV28iVYt2UBHuCkCrkCNdT0O10OsdMJDzW1Rkixw6+0R0MVd0V0WuQgV0MXKO798CRV9bRxdMbQHje3TjS7n0Za+cF0wdq7DjgPFwroDZIgCmkPAjDHpttGO4/zo6YcHy2jbnzbRlld30CnOAmB1qAVoPR1Ay4IjyMLRxJdkg1vvEbQNroq4KwI7ABeovUNsQz7nzHHU78YxdMWQnjSWn0q8LDuNemIObVyQAnbrwpQzQ3Tg9fi5rYF/BV7klLgijX9wIQiYEeKtcn7r5czJKvqo+AxtW7mdtvxxkxq+E+urwytwSyh5BGgu0XI/AlcywXIkM9zSFWLJBXSEFsQcB+BYF+glriBvS7dPGU6FUy+iwcOKaFBRPg3qlkUD89NpIFv4fmzdnQAez1OLK6POLK91bBPLq0IGFxd8AjHSsN5mlVt4/PlYVROV8qQ2paeq6DDPr3d4zW4q4WE7fAsdYAJWAVbiANk7DpiRJjAjVIflMGkFfZAqgrbqC+AF6Ahl8QU20mS7vo86WXIzyfm1K6jfZQNpQEEm5fP0w1lZaZTNw47Z7L9ns4XPZl8+i61+NsOdwQA3svWtY9+4ni1xHfvJdTw8V1fPS2Mz1Vc0UNVHpXTkT1voWE2DB5AAFFCK1RVgBWR9Xc8j+wjMEnJRyS2pBLfekzrkiCtQOdThFaB9wS0nhYRShne5so5jIy4CwCACmncolhWQCpwIJS7wAmyJ+8rrXS5nTx3RFZ46rfZsqQCohwK7Hgr4HaXpZUgcR9PjWBfo9LikIRSQBWZ93V8a0vUyJI5jpKRA6UY8NSAgeoeAGmmBQu/9UDrSvAXwQQRCPRRQA4X6PhI/V6r59al0oxZPDQisSPUVlzQ9lLx6iLi3AEiIHgqk3qHk886rCjA/7TWADjESugZ0vUncO5RSJV3W9VBAlTRZ9w6xXdIkrwkDaKAjxQfY1WwOoIFQdGvADaDMzmz+f6SMYEX4z7hMAAAAAElFTkSuQmCC' // @suppress longLineCheck + }; + + return { + FaviconsByHue, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/file.html b/chromium/third_party/catapult/tracing/tracing/ui/base/file.html new file mode 100644 index 00000000000..0c4945933f1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/file.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + function readFile(fileBlob) { + return new Promise(function(resolve, reject) { + const reader = new FileReader(); + const filename = fileBlob.name; + reader.onload = function(data) { + resolve(data.target.result); + }; + reader.onerror = function(err) { + reject(err); + }; + + const isBinary = filename.endsWith('.gz') || filename.endsWith('.zip'); + if (isBinary) { + reader.readAsArrayBuffer(fileBlob); + } else { + reader.readAsText(fileBlob); + } + }); + } + return { + readFile, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table.html b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table.html new file mode 100644 index 00000000000..942d83f9542 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table.html @@ -0,0 +1,229 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-b-grouping-table'> + <template> + <style> + :host { + display: flex; + } + #table { + flex: 1 1 auto; + font-size: 12px; + } + </style> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + function Row(title, data, groupingKeyFuncs, rowStatsConstructor) { + this.title = title; + this.data_ = data; + if (groupingKeyFuncs === undefined) { + groupingKeyFuncs = []; + } + this.groupingKeyFuncs_ = groupingKeyFuncs; + this.rowStatsConstructor_ = rowStatsConstructor; + + this.subRowsBuilt_ = false; + this.subRows_ = undefined; + + this.rowStats_ = undefined; + } + + Row.prototype = { + getCurrentGroupingKeyFunc_() { + if (this.groupingKeyFuncs_.length === 0) return undefined; + return this.groupingKeyFuncs_[0]; + }, + + get data() { + return this.data_; + }, + + get rowStats() { + if (this.rowStats_ === undefined) { + this.rowStats_ = new this.rowStatsConstructor_(this); + } + return this.rowStats_; + }, + + rebuildSubRowsIfNeeded_() { + if (this.subRowsBuilt_) return; + this.subRowsBuilt_ = true; + + const groupingKeyFunc = this.getCurrentGroupingKeyFunc_(); + if (groupingKeyFunc === undefined) { + this.subRows_ = undefined; + return; + } + + const dataByKey = {}; + let hasValues = false; + this.data_.forEach(function(datum) { + const key = groupingKeyFunc(datum); + hasValues = hasValues || (key !== undefined); + if (dataByKey[key] === undefined) { + dataByKey[key] = []; + } + dataByKey[key].push(datum); + }); + if (!hasValues) { + this.subRows_ = undefined; + return; + } + + this.subRows_ = []; + for (const key in dataByKey) { + const row = new Row(key, + dataByKey[key], + this.groupingKeyFuncs_.slice(1), + this.rowStatsConstructor_); + this.subRows_.push(row); + } + }, + + get isExpanded() { + return (this.subRows && + (this.subRows.length > 0) && + (this.subRows.length < 5)); + }, + + get subRows() { + this.rebuildSubRowsIfNeeded_(); + return this.subRows_; + } + }; + + Polymer({ + is: 'tr-ui-b-grouping-table', + + created() { + this.dataToGroup_ = undefined; + this.groupBy_ = undefined; + this.rowStatsConstructor_ = undefined; + }, + + get tableColumns() { + return this.$.table.tableColumns; + }, + + set tableColumns(tableColumns) { + this.$.table.tableColumns = tableColumns; + }, + + get tableRows() { + return this.$.table.tableRows; + }, + + get sortColumnIndex() { + return this.$.table.sortColumnIndex; + }, + + set sortColumnIndex(sortColumnIndex) { + this.$.table.sortColumnIndex = sortColumnIndex; + }, + + get sortDescending() { + return this.$.table.sortDescending; + }, + + set sortDescending(sortDescending) { + this.$.table.sortDescending = sortDescending; + }, + + get selectionMode() { + return this.$.table.selectionMode; + }, + + set selectionMode(selectionMode) { + this.$.table.selectionMode = selectionMode; + }, + + get rowHighlightStyle() { + return this.$.table.rowHighlightStyle; + }, + + set rowHighlightStyle(rowHighlightStyle) { + this.$.table.rowHighlightStyle = rowHighlightStyle; + }, + + get cellHighlightStyle() { + return this.$.table.cellHighlightStyle; + }, + + set cellHighlightStyle(cellHighlightStyle) { + this.$.table.cellHighlightStyle = cellHighlightStyle; + }, + + get selectedColumnIndex() { + return this.$.table.selectedColumnIndex; + }, + + set selectedColumnIndex(selectedColumnIndex) { + this.$.table.selectedColumnIndex = selectedColumnIndex; + }, + + get selectedTableRow() { + return this.$.table.selectedTableRow; + }, + + set selectedTableRow(selectedTableRow) { + this.$.table.selectedTableRow = selectedTableRow; + }, + + get groupBy() { + return this.groupBy_; + }, + + set groupBy(groupBy) { + this.groupBy_ = groupBy; + this.updateContents_(); + }, + + get dataToGroup() { + return this.dataToGroup_; + }, + + set dataToGroup(dataToGroup) { + this.dataToGroup_ = dataToGroup; + this.updateContents_(); + }, + + get rowStatsConstructor() { + return this.rowStatsConstructor_; + }, + + set rowStatsConstructor(rowStatsConstructor) { + this.rowStatsConstructor_ = rowStatsConstructor; + this.updateContents_(); + }, + + rebuild() { + this.$.table.rebuild(); + }, + + updateContents_() { + const groupBy = this.groupBy_ || []; + const dataToGroup = this.dataToGroup_ || []; + const rowStatsConstructor = this.rowStatsConstructor_ || function() {}; + + const superRow = new Row('', dataToGroup, groupBy, + rowStatsConstructor); + this.$.table.tableRows = superRow.subRows || []; + } + }); + + return { + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker.html new file mode 100644 index 00000000000..6d2f1b917e6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker.html @@ -0,0 +1,258 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/dropdown.html"> + +<dom-module id='tr-ui-b-grouping-table-groupby-picker'> + <template> + <style> + #container { + display: flex; + } + #container *:not(:first-child) { + padding-left: 3px; + border-left: 1px solid black; + margin-left: 3px; + } + </style> + + <div id="container"></div> + </template> +</dom-module> + +<dom-module id="tr-ui-b-grouping-table-groupby-picker-group"> + <template> + <style> + :host { + white-space: nowrap; + } + #left, #right { + user-select: none; + cursor: pointer; + } + </style> + + <span id="left" on-click="moveLeft_">◀</span> + <input type="checkbox" id="enabled" on-change="onEnableChanged_"> + <label for="enabled" id="label"></label> + <span id="right" on-click="moveRight_">▶</span> + </template> +</dom-module> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const THIS_DOC = document.currentScript.ownerDocument; + + Polymer({ + is: 'tr-ui-b-grouping-table-groupby-picker-group', + + created() { + this.picker_ = undefined; + this.group_ = undefined; + }, + + get picker() { + return this.picker_; + }, + + set picker(picker) { + this.picker_ = picker; + }, + + get group() { + return this.group_; + }, + + set group(g) { + this.group_ = g; + this.$.label.textContent = g.label; + }, + + get enabled() { + return this.$.enabled.checked; + }, + + set enabled(enabled) { + this.$.enabled.checked = enabled; + if (!this.enabled) { + this.$.left.style.display = 'none'; + this.$.right.style.display = 'none'; + } + }, + + set isFirst(isFirst) { + this.$.left.style.display = (!this.enabled || isFirst) ? 'none' : + 'inline'; + }, + + set isLast(isLast) { + this.$.right.style.display = (!this.enabled || isLast) ? 'none' : + 'inline'; + }, + + moveLeft_() { + this.picker.moveLeft_(this); + }, + + moveRight_() { + this.picker.moveRight_(this); + }, + + onEnableChanged_() { + if (!this.enabled) { + this.$.left.style.display = 'none'; + this.$.right.style.display = 'none'; + } + this.picker.onEnableChanged_(this); + } + }); + + Polymer({ + is: 'tr-ui-b-grouping-table-groupby-picker', + + created() { + this.settingsKey_ = undefined; + }, + + get settingsKey() { + return this.settingsKey_; + }, + + set settingsKey(settingsKey) { + this.settingsKey_ = settingsKey; + if (this.$.container.children.length) { + this.restoreSetting_(); + } + }, + + restoreSetting_() { + if (this.settingsKey_ === undefined) return; + this.currentGroupKeys = tr.b.Settings.get(this.settingsKey_, + this.currentGroupKeys); + }, + + get possibleGroups() { + return [...this.$.container.children].map(groupEl => groupEl.group); + }, + + set possibleGroups(possibleGroups) { + Polymer.dom(this.$.container).textContent = ''; + for (let i = 0; i < possibleGroups.length; ++i) { + const groupEl = document.createElement( + 'tr-ui-b-grouping-table-groupby-picker-group'); + groupEl.picker = this; + groupEl.group = possibleGroups[i]; + Polymer.dom(this.$.container).appendChild(groupEl); + } + this.restoreSetting_(); + this.updateFirstLast_(); + }, + + updateFirstLast_() { + const groupEls = this.$.container.children; + const enabledGroupEls = [...groupEls].filter(el => el.enabled); + for (let i = 0; i < enabledGroupEls.length; ++i) { + enabledGroupEls[i].isFirst = i === 0; + enabledGroupEls[i].isLast = i === enabledGroupEls.length - 1; + } + }, + + get currentGroupKeys() { + return this.currentGroups.map(group => group.key); + }, + + get currentGroups() { + const groups = []; + for (const groupEl of this.$.container.children) { + if (groupEl.enabled) { + groups.push(groupEl.group); + } + } + return groups; + }, + + set currentGroupKeys(newKeys) { + if (!tr.b.compareArrays(this.currentGroupKeys, newKeys, + (x, y) => x.localeCompare(y))) { + return; + } + + const possibleGroups = new Map(); + for (const group of this.possibleGroups) { + possibleGroups.set(group.key, group); + } + + const groupEls = this.$.container.children; + + let i = 0; + for (i = 0; i < newKeys.length; ++i) { + const group = possibleGroups.get(newKeys[i]); + if (group === undefined) { + newKeys.splice(i, 1); + --i; + continue; + } + groupEls[i].group = group; + groupEls[i].enabled = true; + possibleGroups.delete(newKeys[i]); + } + + for (const group of possibleGroups.values()) { + groupEls[i].group = group; + groupEls[i].enabled = false; + ++i; + } + + this.updateFirstLast_(); + this.onCurrentGroupsChanged_(); + }, + + moveLeft_(groupEl) { + const reference = groupEl.previousSibling; + Polymer.dom(this.$.container).removeChild(groupEl); + Polymer.dom(this.$.container).insertBefore(groupEl, reference); + this.updateFirstLast_(); + + if (groupEl.enabled) { + this.onCurrentGroupsChanged_(); + } + }, + + moveRight_(groupEl) { + const reference = groupEl.nextSibling.nextSibling; + Polymer.dom(this.$.container).removeChild(groupEl); + if (reference) { + Polymer.dom(this.$.container).insertBefore(groupEl, reference); + } else { + Polymer.dom(this.$.container).appendChild(groupEl); + } + this.updateFirstLast_(); + + if (groupEl.enabled) { + this.onCurrentGroupsChanged_(); + } + }, + + onCurrentGroupsChanged_() { + this.dispatchEvent(new tr.b.Event('current-groups-changed')); + tr.b.Settings.set(this.settingsKey_, this.currentGroupKeys); + }, + + onEnableChanged_(groupEl) { + this.updateFirstLast_(); + this.onCurrentGroupsChanged_(); + } + }); + + return { + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker_test.html new file mode 100644 index 00000000000..727d430224f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker_test.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/grouping_table_groupby_picker.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('groupby-picker', function() { + const settingsKey = 'tr-ui-b-grouping-table-groupby-picker-test'; + const picker = document.createElement( + 'tr-ui-b-grouping-table-groupby-picker'); + tr.b.Settings.set(settingsKey, []); + picker.settingsKey = settingsKey; + picker.possibleGroups = [ + {key: 'a', label: 'A'}, + {key: 'b', label: 'B'}, + {key: 'c', label: 'C'}, + {key: 'd', label: 'D'}, + {key: 'e', label: 'E'} + ]; + assert.deepEqual([], picker.currentGroupKeys); + this.addHTMLOutput(picker); + + let keys = ['a', 'b', 'c', 'd', 'e']; + picker.currentGroupKeys = keys; + assert.deepEqual(keys, picker.currentGroupKeys); + + keys = ['e', 'd', 'c', 'b', 'a']; + picker.currentGroupKeys = keys; + assert.deepEqual(keys, picker.currentGroupKeys); + + keys = []; + picker.currentGroupKeys = keys; + assert.deepEqual(keys, picker.currentGroupKeys); + + keys = ['a', 'b', 'd']; + picker.currentGroupKeys = keys; + assert.deepEqual(keys, picker.currentGroupKeys); + + tr.b.Settings.set(settingsKey, ['foo']); + picker.settingsKey = settingsKey; + assert.deepEqual([], picker.currentGroupKeys); + + tr.b.Settings.set(settingsKey, ['e']); + picker.settingsKey = settingsKey; + assert.deepEqual(['e'], picker.currentGroupKeys); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/heading.html b/chromium/third_party/catapult/tracing/tracing/ui/base/heading.html new file mode 100644 index 00000000000..9b625c61b03 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/heading.html @@ -0,0 +1,139 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel='import' href='/tracing/ui/base/constants.html'> + +<dom-module id='tr-ui-b-heading'> + <template> + <style> + :host { + background-color: rgb(243, 245, 247); + border-right: 1px solid #8e8e8e; + display: block; + height: 100%; + margin: 0; + padding: 0 5px 0 0; + } + + heading { + display: block; + overflow-x: hidden; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + } + + #arrow { + flex: 0 0 auto; + font-family: sans-serif; + margin-left: 5px; + margin-right: 5px; + width: 8px; + } + + #link, #heading_content { + display: none; + } + </style> + <heading id='heading' on-click='onHeadingDivClicked_'> + <span id='arrow'></span> + <span id='heading_content'></span> + <tr-ui-a-analysis-link id='link'></tr-ui-a-analysis-link> + </heading> + </template> +</dom-module> +<script> +'use strict'; +Polymer({ + is: 'tr-ui-b-heading', + + DOWN_ARROW: String.fromCharCode(0x25BE), + RIGHT_ARROW: String.fromCharCode(0x25B8), + + ready(viewport) { + // Minus 6 === 1px border + 5px padding right. + this.style.width = (tr.ui.b.constants.HEADING_WIDTH - 6) + 'px'; + + this.heading_ = ''; + this.expanded_ = true; + this.arrowVisible_ = false; + this.selectionGenerator_ = undefined; + + this.updateContents_(); + }, + + get heading() { + return this.heading_; + }, + + set heading(text) { + if (this.heading_ === text) return; + + this.heading_ = text; + this.updateContents_(); + }, + + set arrowVisible(val) { + if (this.arrowVisible_ === val) return; + + this.arrowVisible_ = !!val; + this.updateContents_(); + }, + + set tooltip(text) { + this.$.heading.title = text; + }, + + set selectionGenerator(generator) { + if (this.selectionGenerator_ === generator) return; + + this.selectionGenerator_ = generator; + this.updateContents_(); + }, + + get expanded() { + return this.expanded_; + }, + + set expanded(expanded) { + if (this.expanded_ === expanded) return; + + this.expanded_ = !!expanded; + this.updateContents_(); + }, + + onHeadingDivClicked_() { + this.dispatchEvent(new tr.b.Event('heading-clicked', true)); + }, + + updateContents_() { + if (this.arrowVisible_) { + this.$.arrow.style.display = ''; + } else { + this.$.arrow.style.display = 'none'; + this.$.heading.style.display = this.expanded_ ? '' : 'none'; + } + + if (this.arrowVisible_) { + Polymer.dom(this.$.arrow).textContent = + this.expanded_ ? this.DOWN_ARROW : this.RIGHT_ARROW; + } + + this.$.link.style.display = 'none'; + this.$.heading_content.style.display = 'none'; + + if (this.selectionGenerator_) { + this.$.link.style.display = 'inline-block'; + this.$.link.selection = this.selectionGenerator_; + Polymer.dom(this.$.link).textContent = this.heading_; + } else { + this.$.heading_content.style.display = 'inline-block'; + Polymer.dom(this.$.heading_content).textContent = this.heading_; + } + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/hot_key.html b/chromium/third_party/catapult/tracing/tracing/ui/base/hot_key.html new file mode 100644 index 00000000000..3ffd96bfbe3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/hot_key.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/guid.html"> +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + function HotKey(dict) { + if (dict.eventType === undefined) { + throw new Error('eventType must be given'); + } + if (dict.keyCode === undefined && dict.keyCodes === undefined) { + throw new Error('keyCode or keyCodes must be given'); + } + if (dict.keyCode !== undefined && dict.keyCodes !== undefined) { + throw new Error('Only keyCode or keyCodes can be given'); + } + if (dict.callback === undefined) { + throw new Error('callback must be given'); + } + + this.eventType_ = dict.eventType; + this.keyCodes_ = []; + + if (dict.keyCode) { + this.pushKeyCode_(dict.keyCode); + } else if (dict.keyCodes) { + dict.keyCodes.forEach(this.pushKeyCode_, this); + } + + this.useCapture_ = !!dict.useCapture; + this.callback_ = dict.callback; + this.thisArg_ = dict.thisArg !== undefined ? dict.thisArg : undefined; + + this.helpText_ = dict.helpText !== undefined ? dict.helpText : undefined; + } + + HotKey.prototype = { + get eventType() { + return this.eventType_; + }, + + get keyCodes() { + return this.keyCodes_; + }, + + get helpText() { + return this.helpText_; + }, + + call(e) { + this.callback_.call(this.thisArg_, e); + }, + + pushKeyCode_(keyCode) { + this.keyCodes_.push(keyCode); + } + }; + + return { + HotKey, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller.html new file mode 100644 index 00000000000..f56631dc350 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller.html @@ -0,0 +1,310 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/guid.html"> +<link rel="import" href="/tracing/ui/base/hot_key.html"> + +<dom-module id='tv-ui-b-hotkey-controller'> + <template> + <div></div> + </template> +</dom-module> +<script> +'use strict'; +Polymer({ + is: 'tv-ui-b-hotkey-controller', + + created() { + this.isAttached_ = false; + this.globalMode_ = false; + this.slavedToParentController_ = undefined; + this.curHost_ = undefined; + this.childControllers_ = []; + + this.bubblingKeyDownHotKeys_ = {}; + this.capturingKeyDownHotKeys_ = {}; + this.bubblingKeyPressHotKeys_ = {}; + this.capturingKeyPressHotKeys_ = {}; + + this.onBubblingKeyDown_ = this.onKey_.bind(this, false); + this.onCapturingKeyDown_ = this.onKey_.bind(this, true); + this.onBubblingKeyPress_ = this.onKey_.bind(this, false); + this.onCapturingKeyPress_ = this.onKey_.bind(this, true); + }, + + attached() { + this.isAttached_ = true; + + const host = this.findHost_(); + if (host.__hotkeyController) { + throw new Error('Multiple hotkey controllers attached to this host'); + } + + host.__hotkeyController = this; + this.curHost_ = host; + + let parentElement; + if (host.parentElement) { + parentElement = host.parentElement; + } else { + parentElement = Polymer.dom(host).parentNode.host; + } + const parentController = tr.b.getHotkeyControllerForElement( + parentElement); + + if (parentController) { + this.slavedToParentController_ = parentController; + parentController.addChildController_(this); + return; + } + + host.addEventListener('keydown', this.onBubblingKeyDown_, false); + host.addEventListener('keydown', this.onCapturingKeyDown_, true); + host.addEventListener('keypress', this.onBubblingKeyPress_, false); + host.addEventListener('keypress', this.onCapturingKeyPress_, true); + }, + + detached() { + this.isAttached_ = false; + + const host = this.curHost_; + if (!host) return; + + delete host.__hotkeyController; + this.curHost_ = undefined; + + if (this.slavedToParentController_) { + this.slavedToParentController_.removeChildController_(this); + this.slavedToParentController_ = undefined; + return; + } + + host.removeEventListener('keydown', this.onBubblingKeyDown_, false); + host.removeEventListener('keydown', this.onCapturingKeyDown_, true); + host.removeEventListener('keypress', this.onBubblingKeyPress_, false); + host.removeEventListener('keypress', this.onCapturingKeyPress_, true); + }, + + addChildController_(controller) { + const i = this.childControllers_.indexOf(controller); + if (i !== -1) { + throw new Error('Controller already registered'); + } + this.childControllers_.push(controller); + }, + + removeChildController_(controller) { + const i = this.childControllers_.indexOf(controller); + if (i === -1) { + throw new Error('Controller not registered'); + } + this.childControllers_.splice(i, 1); + return controller; + }, + + getKeyMapForEventType_(eventType, useCapture) { + if (eventType === 'keydown') { + if (!useCapture) { + return this.bubblingKeyDownHotKeys_; + } + return this.capturingKeyDownHotKeys_; + } + if (eventType === 'keypress') { + if (!useCapture) { + return this.bubblingKeyPressHotKeys_; + } + return this.capturingKeyPressHotKeys_; + } + + throw new Error('Unsupported key event'); + }, + + addHotKey(hotKey) { + if (!(hotKey instanceof tr.ui.b.HotKey)) { + throw new Error('hotKey must be a tr.ui.b.HotKey'); + } + + const keyMap = this.getKeyMapForEventType_( + hotKey.eventType, hotKey.useCapture); + + for (let i = 0; i < hotKey.keyCodes.length; i++) { + const keyCode = hotKey.keyCodes[i]; + if (keyMap[keyCode]) { + throw new Error('Key is already bound for keyCode=' + keyCode); + } + } + + for (let i = 0; i < hotKey.keyCodes.length; i++) { + const keyCode = hotKey.keyCodes[i]; + keyMap[keyCode] = hotKey; + } + return hotKey; + }, + + removeHotKey(hotKey) { + if (!(hotKey instanceof tr.ui.b.HotKey)) { + throw new Error('hotKey must be a tr.ui.b.HotKey'); + } + + const keyMap = this.getKeyMapForEventType_( + hotKey.eventType, hotKey.useCapture); + + for (let i = 0; i < hotKey.keyCodes.length; i++) { + const keyCode = hotKey.keyCodes[i]; + if (!keyMap[keyCode]) { + throw new Error('Key is not bound for keyCode=' + keyCode); + } + keyMap[keyCode] = hotKey; + } + for (let i = 0; i < hotKey.keyCodes.length; i++) { + const keyCode = hotKey.keyCodes[i]; + delete keyMap[keyCode]; + } + return hotKey; + }, + + get globalMode() { + return this.globalMode_; + }, + + set globalMode(globalMode) { + const wasAttached = this.isAttached_; + if (wasAttached) { + this.detached(); + } + this.globalMode_ = !!globalMode; + if (wasAttached) { + this.attached(); + } + }, + + get topmostConroller_() { + if (this.slavedToParentController_) { + return this.slavedToParentController_.topmostConroller_; + } + return this; + }, + + childRequestsGeneralFocus(child) { + const topmost = this.topmostConroller_; + if (topmost.curHost_) { + if (topmost.curHost_.hasAttribute('tabIndex')) { + topmost.curHost_.focus(); + } else { + if (document.activeElement) { + document.activeElement.blur(); + } + } + } else { + if (document.activeElement) { + document.activeElement.blur(); + } + } + }, + + childRequestsBlur(child) { + child.blur(); + + const topmost = this.topmostConroller_; + if (topmost.curHost_) { + topmost.curHost_.focus(); + } + }, + + findHost_() { + if (this.globalMode_) return document.body; + if (this.parentElement) return this.parentElement; + if (!Polymer.dom(this).parentNode) return this.host; + + let node = this.parentNode; + while (Polymer.dom(node).parentNode) node = Polymer.dom(node).parentNode; + return node.host; + }, + + appendMatchingHotKeysTo_(matchedHotKeys, + useCapture, e) { + const localKeyMap = this.getKeyMapForEventType_(e.type, useCapture); + const localHotKey = localKeyMap[e.keyCode]; + if (localHotKey) { + matchedHotKeys.push(localHotKey); + } + + for (let i = 0; i < this.childControllers_.length; i++) { + const controller = this.childControllers_[i]; + controller.appendMatchingHotKeysTo_(matchedHotKeys, + useCapture, e); + } + }, + + onKey_(useCapture, e) { + // Keys dispatched to INPUT elements still bubble, even when they're + // handled. So, skip any events that targeted the input element. + if (!useCapture && e.path[0].tagName === 'INPUT') return; + + let sortedControllers; + + const matchedHotKeys = []; + this.appendMatchingHotKeysTo_(matchedHotKeys, useCapture, e); + + if (matchedHotKeys.length === 0) return false; + + if (matchedHotKeys.length > 1) { + // TODO(nduca): To do support for coddling hotKeys, we need to + // sort the listeners by their capturing/bubbling order and then pick + // the one that would topologically win the tie, per DOM dispatch rules. + throw new Error('More than one hotKey is currently unsupported'); + } + + + const hotKey = matchedHotKeys[0]; + + let prevented = 0; + prevented |= hotKey.call(e); + + // We want to return false if preventDefaulted, or one of the handlers + // return false. But otherwise, we want to return undefiend. + return !prevented && e.defaultPrevented; + } +}); +</script> +<script> +'use strict'; +tr.exportTo('tr.b', function() { + function getHotkeyControllerForElement(refElement) { + let curElement = refElement; + while (curElement) { + if (curElement.tagName === 'tv-ui-b-hotkey-controller') { + return curElement; + } + + if (curElement.__hotkeyController) { + return curElement.__hotkeyController; + } + + if (curElement.parentElement) { + curElement = curElement.parentElement; + continue; + } + + // Probably inside a shadow + curElement = findHost(curElement); + } + return undefined; + } + + function findHost(initialNode) { + let node = initialNode; + while (Polymer.dom(node).parentNode) { + node = Polymer.dom(node).parentNode; + } + return node.host; + } + + return { + getHotkeyControllerForElement, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller_test.html new file mode 100644 index 00000000000..cff10cb775c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller_test.html @@ -0,0 +1,139 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/event.html"> +<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const KeyEventManager = tr.b.KeyEventManager; + + function newKeyEvent(eventType, dict) { + const e = new tr.b.Event(eventType, true, true); + if (dict.keyCode === undefined) { + throw new Error('keyCode required'); + } + e.keyCode = dict.keyCode; + return e; + } + + test('simpleHotkeyManager', function() { + const rootElement = document.createElement('div'); + Polymer.dom(document.body).appendChild(rootElement); + try { + const elementShadow = rootElement.createShadowRoot(); + + const hkc = document.createElement('tv-ui-b-hotkey-controller'); + Polymer.dom(elementShadow).appendChild(hkc); + + const subElement = document.createElement('div'); + Polymer.dom(elementShadow).appendChild(subElement); + + assert.strictEqual(tr.b.getHotkeyControllerForElement(subElement), hkc); + + let didGetCalled = false; + hkc.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keydown', + keyCode: 73, useCapture: true, + callback() { + didGetCalled = true; + } + })); + + // Ensure it is called when events target the root element. + let e = newKeyEvent('keydown', {keyCode: 73}); + rootElement.dispatchEvent(e); + assert.isTrue(didGetCalled); + + // Ensure it is still called when we target the sub element. + didGetCalled = false; + e = newKeyEvent('keydown', {keyCode: 73}); + subElement.dispatchEvent(e); + assert.isTrue(didGetCalled); + } finally { + Polymer.dom(document.body).removeChild(rootElement); + } + }); + + test('nestedHotkeyController', function() { + const rootElement = document.createElement('div'); + Polymer.dom(document.body).appendChild(rootElement); + try { + const elementShadow = rootElement.createShadowRoot(); + + const hkc = document.createElement('tv-ui-b-hotkey-controller'); + Polymer.dom(elementShadow).appendChild(hkc); + + const subElement = document.createElement('div'); + Polymer.dom(elementShadow).appendChild(subElement); + assert.strictEqual( + tr.b.getHotkeyControllerForElement(elementShadow), hkc); + + const subHKC = document.createElement('tv-ui-b-hotkey-controller'); + Polymer.dom(subElement).appendChild(subHKC); + + assert.strictEqual( + tr.b.getHotkeyControllerForElement(subElement), subHKC); + + let didGetCalled = false; + subHKC.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keydown', + keyCode: 73, useCapture: true, + callback() { + didGetCalled = true; + } + })); + + // Ensure it is called when events target the root element. + const e = newKeyEvent('keydown', {keyCode: 73}); + rootElement.dispatchEvent(e); + assert.isTrue(didGetCalled); + } finally { + Polymer.dom(document.body).removeChild(rootElement); + } + }); + + test('inputInsideHKC', function() { + const rootElement = document.createElement('div'); + Polymer.dom(document.body).appendChild(rootElement); + try { + const elementShadow = rootElement.createShadowRoot(); + + const hkc = document.createElement('tv-ui-b-hotkey-controller'); + Polymer.dom(elementShadow).appendChild(hkc); + + const inputEl = document.createElement('input'); + Polymer.dom(elementShadow).appendChild(inputEl); + + let didGetCalled = false; + hkc.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keypress', + keyCode: 'a'.charCodeAt(0), useCapture: false, + callback() { + didGetCalled = true; + } + })); + + // Ensure it is called when events target the root element. + didGetCalled = false; + let e = newKeyEvent('keypress', {keyCode: 'a'.charCodeAt(0)}); + rootElement.dispatchEvent(e); + assert.isTrue(didGetCalled); + + // Handler should NOT be called when events target the input element. + didGetCalled = false; + e = newKeyEvent('keypress', {keyCode: 'a'.charCodeAt(0)}); + inputEl.dispatchEvent(e); + assert.isFalse(didGetCalled); + } finally { + Polymer.dom(document.body).removeChild(rootElement); + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar.html b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar.html new file mode 100644 index 00000000000..3a4894278be --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<dom-module id='tr-ui-b-info-bar'> + <template> + <style> + :host { + align-items: center; + flex: 0 0 auto; + background-color: rgb(252, 235, 162); + border-bottom: 1px solid #A3A3A3; + border-left: 1px solid white; + border-right: 1px solid #A3A3A3; + border-top: 1px solid white; + display: flex; + height: 26px; + padding: 0 3px 0 3px; + } + + :host([hidden]) { + display: none !important; + } + + #message { flex: 1 1 auto; } + </style> + + <span id='message'></span> + <span id='buttons'></span> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-b-info-bar', + + ready() { + this.messageEl_ = this.$.message; + this.buttonsEl_ = this.$.buttons; + + this.message = ''; + }, + + get message() { + return Polymer.dom(this.messageEl_).textContent; + }, + + set message(message) { + Polymer.dom(this.messageEl_).textContent = message; + }, + + get visible() { + return !this.hidden; + }, + + set visible(visible) { + this.hidden = !visible; + }, + + removeAllButtons() { + Polymer.dom(this.buttonsEl_).textContent = ''; + }, + + addButton(text, clickCallback) { + const button = document.createElement('button'); + Polymer.dom(button).textContent = text; + button.addEventListener('click', event => clickCallback(event, this)); + Polymer.dom(this.buttonsEl_).appendChild(button); + return button; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group.html b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group.html new file mode 100644 index 00000000000..d095e4d99f9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel='import' href='/tracing/ui/base/info_bar.html'> + +<dom-module id='tr-ui-b-info-bar-group'> + <template> + <style> + :host { + flex: 0 0 auto; + flex-direction: column; + display: flex; + } + </style> + <div id='messages'></div> + </template> +</dom-module> +<script> +'use strict'; +Polymer({ + is: 'tr-ui-b-info-bar-group', + + ready() { + this.messages_ = []; + }, + + clearMessages() { + this.messages_ = []; + this.updateContents_(); + }, + + addMessage(text, opt_buttons) { + opt_buttons = opt_buttons || []; + for (let i = 0; i < opt_buttons.length; i++) { + if (opt_buttons[i].buttonText === undefined) { + throw new Error('buttonText must be provided'); + } + if (opt_buttons[i].onClick === undefined) { + throw new Error('onClick must be provided'); + } + } + + this.messages_.push({ + text, + buttons: opt_buttons || [] + }); + this.updateContents_(); + }, + + updateContents_() { + Polymer.dom(this.$.messages).textContent = ''; + this.messages_.forEach(function(message) { + const bar = document.createElement('tr-ui-b-info-bar'); + bar.message = message.text; + bar.visible = true; + + message.buttons.forEach(function(button) { + bar.addButton(button.buttonText, button.onClick); + }, this); + + Polymer.dom(this.$.messages).appendChild(bar); + }, this); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group_test.html new file mode 100644 index 00000000000..304d48ede37 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group_test.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/base/info_bar_group.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('group-instantiate', function() { + const infoBarGroup = document.createElement('tr-ui-b-info-bar-group'); + infoBarGroup.addMessage( + 'Message 1', + [{buttonText: 'ok', onClick() {}}]); + infoBarGroup.addMessage( + 'Message 2', + [{buttonText: 'button 2', onClick() {}}]); + this.addHTMLOutput(infoBarGroup); + }); + + test('group-populate-then-clear', function() { + const infoBarGroup = document.createElement('tr-ui-b-info-bar-group'); + infoBarGroup.addMessage( + 'Message 1', + [{buttonText: 'ok', onClick() {}}]); + infoBarGroup.addMessage( + 'Message 2', + [{buttonText: 'button 2', onClick() {}}]); + infoBarGroup.clearMessages(); + assert.strictEqual(infoBarGroup.children.length, 0); + }); + + test('group-populate-clear-repopulate', function() { + const infoBarGroup = document.createElement('tr-ui-b-info-bar-group'); + infoBarGroup.addMessage( + 'Message 1', + [{buttonText: 'ok', onClick() {}}]); + infoBarGroup.addMessage( + 'Message 2', + [{buttonText: 'button 2', onClick() {}}]); + infoBarGroup.clearMessages(); + infoBarGroup.addMessage( + 'Message 1', + [{buttonText: 'ok', onClick() {}}]); + this.addHTMLOutput(infoBarGroup); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_test.html new file mode 100644 index 00000000000..94b19482ec8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_test.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/base/info_bar.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const infoBar = document.createElement('tr-ui-b-info-bar'); + infoBar.message = 'This is an info'; + infoBar.visible = true; + this.addHTMLOutput(infoBar); + }); + + test('buttons', function() { + const infoBar = document.createElement('tr-ui-b-info-bar'); + infoBar.visible = true; + infoBar.message = 'This is an info bar with buttons'; + let didClick = false; + const button = infoBar.addButton('More info...', function() { + didClick = true; + }); + button.click(); + assert.isTrue(didClick); + this.addHTMLOutput(infoBar); + }); + + test('hiding', function() { + const infoBar = document.createElement('tr-ui-b-info-bar'); + infoBar.message = 'This is an info bar'; + infoBar.visible = true; + this.addHTMLOutput(infoBar); + + assert.strictEqual(getComputedStyle(infoBar).display, 'flex'); + + infoBar.visible = false; + assert.strictEqual(getComputedStyle(infoBar).display, 'none'); + + infoBar.visible = true; + assert.strictEqual(getComputedStyle(infoBar).display, 'flex'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart.html new file mode 100644 index 00000000000..e02f4413bb1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart.html @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/chart_base_2d_brushable_x.html"> +<link rel="import" href="/tracing/ui/base/column_chart.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const LineChart = tr.ui.b.define('line-chart', tr.ui.b.ChartBase2DBrushX); + + LineChart.prototype = { + __proto__: tr.ui.b.ChartBase2DBrushX.prototype, + + decorate() { + super.decorate(); + this.enableHoverBox = true; + this.displayXInHover = false; + }, + + get defaultGraphWidth() { + return 20 * this.data_.length; + }, + + get defaultGraphHeight() { + return 100; + }, + + drawHoverValueBox_(circle) { + tr.ui.b.ColumnChart.prototype.drawHoverValueBox_.call(this, circle); + }, + + clearHoverValueBox_(circle) { + tr.ui.b.ColumnChart.prototype.clearHoverValueBox_.call(this, circle); + }, + + updateDataContents_(dataSel) { + dataSel.selectAll('*').remove(); + const dataBySeriesKey = this.getDataBySeriesKey_(); + const seriesKeys = [...this.seriesByKey_.keys()]; + const pathsSel = dataSel.selectAll('path').data(seriesKeys); + pathsSel.enter() + .append('path') + .style('fill', 'none') + .style('stroke-width', '1.5px') + .style('stroke', key => this.getDataSeries(key).color) + .attr('d', key => { + const line = d3.svg.line() + .x(d => this.xScale_(d.x)) + .y(d => this.yScale_(this.dataRange.clamp(d[key]))); + return line(dataBySeriesKey[key]); + }); + pathsSel.exit().remove(); + + if (this.enableHoverBox) { + for (let index = 0; index < this.data_.length; ++index) { + const datum = this.data_[index]; + const x = this.getXForDatum_(datum, index); + for (const [key, value] of Object.entries(datum)) { + if (key === 'x') continue; + if (value === undefined) continue; + const color = this.getDataSeries(key).color; + const circle = document.createElementNS( + 'http://www.w3.org/2000/svg', 'circle'); + circle.setAttribute('cx', this.xScale_(x)); + circle.setAttribute('cy', + this.yScale_(this.dataRange.clamp(value))); + circle.setAttribute('r', 5); + circle.style.fill = color; + circle.datum = datum; + circle.key = key; + circle.value = datum[key]; + circle.leftPx = this.xScale_(x); + circle.widthPx = 0; + circle.color = color; + circle.topPx = this.yScale_(this.dataRange.clamp(value)); + circle.heightPx = 0; + circle.addEventListener( + 'mouseenter', () => this.drawHoverValueBox_(circle)); + circle.addEventListener( + 'mouseleave', () => this.clearHoverValueBox_(circle)); + dataSel[0][0].appendChild(circle); + } + } + } + } + }; + + return { + LineChart, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart_test.html new file mode 100644 index 00000000000..411e46fb5dc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart_test.html @@ -0,0 +1,180 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/ui/base/line_chart.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(function() { + test('instantiation_singleSeries', function() { + const chart = new tr.ui.b.LineChart(); + chart.displayXInHover = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, y: 100}, + {x: 20, y: 110}, + {x: 30, y: 100}, + {x: 40, y: 50} + ]; + }); + + test('instantiation_twoSeries', function() { + const chart = new tr.ui.b.LineChart(); + chart.displayXInHover = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, alpha: 100, beta: 50}, + {x: 20, alpha: 110, beta: 75}, + {x: 30, alpha: 100, beta: 125}, + {x: 40, alpha: 50, beta: 125} + ]; + + const r = new tr.b.math.Range(); + r.addValue(20); + r.addValue(40); + chart.brushedRange = r; + }); + + test('instantiation_twoSparseSeriesWithFirstValueSparse', function() { + const chart = new tr.ui.b.LineChart(); + chart.displayXInHover = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, alpha: 20, beta: undefined}, + {x: 20, alpha: undefined, beta: 10}, + {x: 30, alpha: 10, beta: undefined}, + {x: 45, alpha: undefined, beta: 20}, + {x: 50, alpha: 30, beta: 30} + ]; + }); + + test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() { + const chart = new tr.ui.b.LineChart(); + chart.displayXInHover = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, alpha: 20, beta: 40}, + {x: 20, alpha: undefined, beta: 10}, + {x: 30, alpha: 10, beta: undefined}, + {x: 45, alpha: undefined, beta: 20}, + {x: 50, alpha: 30, beta: undefined} + ]; + }); + + test('brushRangeFromIndices', function() { + const chart = new tr.ui.b.LineChart(); + chart.displayXInHover = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, value: 50}, + {x: 30, value: 60}, + {x: 70, value: 70}, + {x: 80, value: 80}, + {x: 120, value: 90} + ]; + let r = new tr.b.math.Range(); + + // Range min should be 10. + r = chart.computeBrushRangeFromIndices(-2, 1); + assert.strictEqual(r.min, 10); + + // Range max should be 120. + r = chart.computeBrushRangeFromIndices(3, 10); + assert.strictEqual(r.max, 120); + + // Range should be [10, 120] + r = chart.computeBrushRangeFromIndices(-2, 10); + assert.strictEqual(r.min, 10); + assert.strictEqual(r.max, 120); + + // Range should be [20, 100] + r = chart.computeBrushRangeFromIndices(1, 3); + assert.strictEqual(r.min, 20); + assert.strictEqual(r.max, 100); + }); + + test('instantiation_interactiveBrushing', function() { + const chart = new tr.ui.b.LineChart(); + chart.displayXInHover = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, value: 50}, + {x: 20, value: 60}, + {x: 30, value: 80}, + {x: 40, value: 20}, + {x: 50, value: 30}, + {x: 60, value: 20}, + {x: 70, value: 15}, + {x: 80, value: 20} + ]; + + let mouseDownIndex = undefined; + let curMouseIndex = undefined; + + function updateBrushedRange() { + if (mouseDownIndex === undefined) { + chart.brushedRange = new tr.b.math.Range(); + return; + } + chart.brushedRange = chart.computeBrushRangeFromIndices( + mouseDownIndex, curMouseIndex); + } + + chart.addEventListener('item-mousedown', function(e) { + mouseDownIndex = e.index; + curMouseIndex = e.index; + updateBrushedRange(); + }); + chart.addEventListener('item-mousemove', function(e) { + if (e.button === undefined) return; + curMouseIndex = e.index; + updateBrushedRange(); + }); + chart.addEventListener('item-mouseup', function(e) { + curMouseIndex = e.index; + updateBrushedRange(); + }); + }); + + test('overrideDataRange', function() { + let chart = new tr.ui.b.LineChart(); + chart.displayXInHover = true; + this.addHTMLOutput(chart); + chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(10, 90); + chart.data = [ + {x: 0, value: 0}, + {x: 1, value: 100}, + ]; + + chart = new tr.ui.b.LineChart(); + chart.displayXInHover = true; + this.addHTMLOutput(chart); + chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(-10, 100); + chart.data = [ + {x: 0, value: 0}, + {x: 1, value: 50}, + ]; + }); + + test('sizeInBytes', function() { + const chart = new tr.ui.b.LineChart(); + chart.unit = tr.b.Unit.byName.sizeInBytes; + chart.yLogScaleBase = 2; + chart.graphHeight = 400; + chart.isYLogScale = true; + chart.hideLegend = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 0, value: 1}, + {x: 1, value: 1 << 10}, + {x: 2, value: 1 << 20}, + {x: 3, value: 1 << 30}, + ]; + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/list_view.html b/chromium/third_party/catapult/tracing/tracing/ui/base/list_view.html new file mode 100644 index 00000000000..6e2d1652016 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/list_view.html @@ -0,0 +1,183 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/event.html"> +<link rel="import" href="/tracing/ui/base/container_that_decorates_its_children.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> + +<script> +'use strict'; + +/** + * @fileoverview Simple list view. + */ +tr.exportTo('tr.ui.b', function() { + /** + * @constructor + */ + const ListView = tr.ui.b.define( + 'x-list-view', tr.ui.b.ContainerThatDecoratesItsChildren); + + ListView.prototype = { + __proto__: tr.ui.b.ContainerThatDecoratesItsChildren.prototype, + + decorate() { + tr.ui.b.ContainerThatDecoratesItsChildren.prototype.decorate.call(this); + + Polymer.dom(this).classList.add('x-list-view'); + this.style.display = 'block'; + this.style.userSelect = 'none'; + this.style.outline = 'none'; + this.onItemClicked_ = this.onItemClicked_.bind(this); + this.onKeyDown_ = this.onKeyDown_.bind(this); + this.tabIndex = 0; + this.addEventListener('keydown', this.onKeyDown_); + + this.selectionChanged_ = false; + }, + + decorateChild_(item) { + Polymer.dom(item).classList.add('list-item'); + item.style.paddingTop = '2px'; + item.style.paddingRight = '4px'; + item.style.paddingBottom = '2px'; + item.style.paddingLeft = '4px'; + item.addEventListener('click', this.onItemClicked_, true); + + Object.defineProperty( + item, + 'selected', { + configurable: true, + get: () => item.hasAttribute('selected'), + set: value => { + // |this| is the ListView. + const oldSelection = this.selectedElement; + if (oldSelection && oldSelection !== item && value) { + Polymer.dom(this.selectedElement).removeAttribute('selected'); + } + if (value) { + Polymer.dom(item).setAttribute('selected', 'selected'); + item.style.backgroundColor = 'rgb(171, 217, 202)'; + item.style.outline = '1px dotted rgba(0,0,0,0.1)'; + item.style.outlineOffset = 0; + } else { + Polymer.dom(item).removeAttribute('selected'); + item.style.backgroundColor = ''; + } + const newSelection = this.selectedElement; + if (newSelection !== oldSelection) { + tr.b.dispatchSimpleEvent(this, 'selection-changed', false); + } + }, + }); + }, + + undecorateChild_(item) { + this.selectionChanged_ |= item.selected; + + Polymer.dom(item).classList.remove('list-item'); + item.removeEventListener('click', this.onItemClicked_); + delete item.selected; + }, + + beginDecorating_() { + this.selectionChanged_ = false; + }, + + doneDecoratingForNow_() { + if (this.selectionChanged_) { + tr.b.dispatchSimpleEvent(this, 'selection-changed', false); + } + }, + + get selectedElement() { + const el = Polymer.dom(this).querySelector('.list-item[selected]'); + if (!el) return undefined; + return el; + }, + + set selectedElement(el) { + if (!el) { + if (this.selectedElement) { + this.selectedElement.selected = false; + } + return; + } + + if (el.parentElement !== this) { + throw new Error( + 'Can only select elements that are children of this list view'); + } + el.selected = true; + }, + + getElementByIndex(index) { + return Polymer.dom(this) + .querySelector('.list-item:nth-child(' + index + ')'); + }, + + clear() { + const changed = this.selectedElement !== undefined; + tr.ui.b.ContainerThatDecoratesItsChildren.prototype.clear.call(this); + if (changed) { + tr.b.dispatchSimpleEvent(this, 'selection-changed', false); + } + }, + + onItemClicked_(e) { + const currentSelectedElement = this.selectedElement; + if (currentSelectedElement) { + Polymer.dom(currentSelectedElement).removeAttribute('selected'); + } + let element = e.target; + while (element.parentElement !== this) { + element = element.parentElement; + } + if (element !== currentSelectedElement) { + Polymer.dom(element).setAttribute('selected', 'selected'); + } + tr.b.dispatchSimpleEvent(this, 'selection-changed', false); + }, + + onKeyDown_(e) { + if (this.selectedElement === undefined) return; + + if (e.keyCode === 38) { // Up arrow. + const prev = Polymer.dom(this.selectedElement).previousSibling; + if (prev) { + prev.selected = true; + tr.ui.b.scrollIntoViewIfNeeded(prev); + e.preventDefault(); + return true; + } + } else if (e.keyCode === 40) { // Down arrow. + const next = Polymer.dom(this.selectedElement).nextSibling; + if (next) { + next.selected = true; + tr.ui.b.scrollIntoViewIfNeeded(next); + e.preventDefault(); + return true; + } + } + }, + + addItem(textContent) { + const item = document.createElement('div'); + Polymer.dom(item).textContent = textContent; + Polymer.dom(this).appendChild(item); + item.style.userSelect = 'none'; + return item; + } + + }; + + return { + ListView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/list_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/list_view_test.html new file mode 100644 index 00000000000..685eefc9472 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/list_view_test.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/base/list_view.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ListView = tr.ui.b.ListView; + + test('instantiate', function() { + const view = new ListView(); + const i1 = view.addItem('item 1'); + const i2 = view.addItem('item 2'); + const i3 = view.addItem('item 3'); + this.addHTMLOutput(view); + }); + + test('programmaticSelection', function() { + const view = new ListView(); + const i1 = view.addItem('item 1'); + const i2 = view.addItem('item 2'); + const i3 = view.addItem('item 3'); + + i2.selected = true; + assert.isTrue(i2.hasAttribute('selected')); + i3.selected = true; + assert.isFalse(i2.hasAttribute('selected')); + assert.isTrue(i3.hasAttribute('selected')); + }); + + test('clickSelection', function() { + const view = new ListView(); + let didFireSelectionChange = false; + view.addEventListener('selection-changed', function() { + didFireSelectionChange = true; + }); + const i1 = view.addItem('item 1'); + const i2 = view.addItem('item 2'); + const i3 = view.addItem('item 3'); + + didFireSelectionChange = false; + i2.click(); + assert.isTrue(didFireSelectionChange); + assert.strictEqual(view.selectedElement, i2); + + didFireSelectionChange = false; + i3.click(); + assert.isTrue(didFireSelectionChange); + assert.strictEqual(view.selectedElement, i3); + + // Click the same target again. + didFireSelectionChange = false; + i3.click(); + assert.isTrue(didFireSelectionChange); + assert.isUndefined(view.selectedElement); + + didFireSelectionChange = false; + i1.click(); + assert.isTrue(didFireSelectionChange); + assert.strictEqual(view.selectedElement, i1); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon.html new file mode 100644 index 00000000000..bf32a7f25fe --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon.html @@ -0,0 +1,115 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/mouse_modes.html"> + +<dom-module id='tr-ui-b-mouse-mode-icon'> + <template> + <style> + :host { + display: block; + background-image: url(../images/ui-states.png); + width: 27px; + height: 30px; + } + :host.active { + cursor: auto; + } + </style> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-b-mouse-mode-icon', + + properties: { + modeName: { + type: String, + reflectToAttribute: true, + observer: 'modeNameChanged' + }, + }, + + created() { + this.active_ = false; + this.acceleratorKey_ = undefined; + }, + + ready() { + this.updateContents_(); + }, + + get mode() { + return tr.ui.b.MOUSE_SELECTOR_MODE[this.modeName]; + }, + + set mode(mode) { + const modeInfo = tr.ui.b.MOUSE_SELECTOR_MODE_INFOS[mode]; + if (modeInfo === undefined) { + throw new Error('Unknown mode'); + } + this.modeName = modeInfo.name; + }, + + modeNameChanged() { + this.updateContents_(); + }, + + get active() { + return this.active_; + }, + + set active(active) { + this.active_ = !!active; + if (this.active_) { + Polymer.dom(this).classList.add('active'); + } else { + Polymer.dom(this).classList.remove('active'); + } + this.updateContents_(); + }, + + get acceleratorKey() { + return this.acceleratorKey_; + }, + + set acceleratorKey(acceleratorKey) { + this.acceleratorKey_ = acceleratorKey; + this.updateContents_(); + }, + + updateContents_() { + if (this.modeName === undefined) return; + + const mode = this.mode; + if (mode === undefined) { + throw new Error('Invalid mode'); + } + + const modeInfo = tr.ui.b.MOUSE_SELECTOR_MODE_INFOS[mode]; + if (!modeInfo) { + throw new Error('Invalid mode'); + } + + let title = modeInfo.title; + if (this.acceleratorKey_) { + title = title + ' (' + this.acceleratorKey_ + ')'; + } + this.title = title; + + let bp; + if (this.active_) { + bp = modeInfo.activeBackgroundPosition; + } else { + bp = modeInfo.defaultBackgroundPosition; + } + this.style.backgroundPosition = bp; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon_test.html new file mode 100644 index 00000000000..047d5af22b9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon_test.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const MOUSE_SELECTOR_MODE = tr.ui.b.MOUSE_SELECTOR_MODE; + + test('inactive', function() { + const icon = document.createElement('tr-ui-b-mouse-mode-icon'); + icon.mode = MOUSE_SELECTOR_MODE.SELECTION; + assert.strictEqual(icon.modeName, 'SELECTION'); + icon.acceleratorKey = 'a'; + this.addHTMLOutput(icon); + }); + + test('active', function() { + const icon = document.createElement('tr-ui-b-mouse-mode-icon'); + icon.mode = MOUSE_SELECTOR_MODE.SELECTION; + assert.strictEqual(icon.modeName, 'SELECTION'); + icon.active = true; + this.addHTMLOutput(icon); + }); + + test('modeNameSetter', function() { + const icon = document.createElement('tr-ui-b-mouse-mode-icon'); + Polymer.dom(icon).setAttribute('mode-name', 'SELECTION'); + this.addHTMLOutput(icon); + + return Promise.resolve().then(function() { + assert.strictEqual(icon.mode, 1); + }); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector.html new file mode 100644 index 00000000000..4ee7348833f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector.html @@ -0,0 +1,577 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/event.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> +<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html"> +<link rel="import" href="/tracing/ui/base/mouse_modes.html"> +<link rel="import" href="/tracing/ui/base/mouse_tracker.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> + +<dom-module id='tr-ui-b-mouse-mode-selector'> + <template> + <style> + :host { + + -webkit-user-drag: element; + -webkit-user-select: none; + + background: #DDD; + border: 1px solid #BBB; + border-radius: 4px; + box-shadow: 0 1px 2px rgba(0,0,0,0.2); + left: calc(100% - 120px); + position: absolute; + top: 100px; + user-select: none; + width: 29px; + z-index: 20; + } + + .drag-handle { + background: url(../images/ui-states.png) 2px 3px no-repeat; + background-repeat: no-repeat; + border-bottom: 1px solid #BCBCBC; + cursor: move; + display: block; + height: 13px; + width: 27px; + } + + .tool-button { + background-position: center center; + background-repeat: no-repeat; + border-bottom: 1px solid #BCBCBC; + border-top: 1px solid #F1F1F1; + cursor: pointer; + } + + .buttons > .tool-button:last-child { + border-bottom: none; + } + + </style> + <div class="drag-handle"></div> + <div class="buttons"> + </div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const MOUSE_SELECTOR_MODE = tr.ui.b.MOUSE_SELECTOR_MODE; + const MOUSE_SELECTOR_MODE_INFOS = tr.ui.b.MOUSE_SELECTOR_MODE_INFOS; + + + const MIN_MOUSE_SELECTION_DISTANCE = 4; + + const MODIFIER = { + SHIFT: 0x1, + SPACE: 0x2, + CMD_OR_CTRL: 0x4 + }; + + function isCmdOrCtrlPressed(event) { + if (tr.isMac) return event.metaKey; + return event.ctrlKey; + } + + /** + * Provides a panel for switching the interaction mode of the mouse. + * It handles the user interaction and dispatches events for the various + * modes. + */ + Polymer({ + is: 'tr-ui-b-mouse-mode-selector', + + created() { + this.supportedModeMask_ = MOUSE_SELECTOR_MODE.ALL_MODES; + + this.initialRelativeMouseDownPos_ = {x: 0, y: 0}; + + this.defaultMode_ = MOUSE_SELECTOR_MODE.PANSCAN; + this.settingsKey_ = undefined; + this.mousePos_ = {x: 0, y: 0}; + this.mouseDownPos_ = {x: 0, y: 0}; + + this.onMouseDown_ = this.onMouseDown_.bind(this); + this.onMouseMove_ = this.onMouseMove_.bind(this); + this.onMouseUp_ = this.onMouseUp_.bind(this); + + this.onKeyDown_ = this.onKeyDown_.bind(this); + this.onKeyUp_ = this.onKeyUp_.bind(this); + + this.mode_ = undefined; + this.modeToKeyCodeMap_ = {}; + this.modifierToModeMap_ = {}; + + this.targetElement_ = undefined; + this.modeBeforeAlternativeModeActivated_ = null; + + this.isInteracting_ = false; + this.isClick_ = false; + }, + + ready() { + this.buttonsEl_ = Polymer.dom(this.root).querySelector('.buttons'); + this.dragHandleEl_ = Polymer.dom(this.root).querySelector( + '.drag-handle'); + this.supportedModeMask = MOUSE_SELECTOR_MODE.ALL_MODES; + + this.dragHandleEl_.addEventListener('mousedown', + this.onDragHandleMouseDown_.bind(this)); + + this.buttonsEl_.addEventListener('mouseup', this.onButtonMouseUp_); + this.buttonsEl_.addEventListener('mousedown', this.onButtonMouseDown_); + this.buttonsEl_.addEventListener('click', this.onButtonPress_.bind(this)); + }, + + attached() { + document.addEventListener('keydown', this.onKeyDown_); + document.addEventListener('keyup', this.onKeyUp_); + }, + + detached() { + document.removeEventListener('keydown', this.onKeyDown_); + document.removeEventListener('keyup', this.onKeyUp_); + }, + + get targetElement() { + return this.targetElement_; + }, + + set targetElement(target) { + if (this.targetElement_) { + this.targetElement_.removeEventListener('mousedown', this.onMouseDown_); + } + this.targetElement_ = target; + if (this.targetElement_) { + this.targetElement_.addEventListener('mousedown', this.onMouseDown_); + } + }, + + get defaultMode() { + return this.defaultMode_; + }, + + set defaultMode(defaultMode) { + this.defaultMode_ = defaultMode; + }, + + get settingsKey() { + return this.settingsKey_; + }, + + set settingsKey(settingsKey) { + this.settingsKey_ = settingsKey; + if (!this.settingsKey_) return; + + let mode = tr.b.Settings.get(this.settingsKey_ + '.mode', undefined); + // Modes changed from 1,2,3,4 to 0x1, 0x2, 0x4, 0x8. Fix any stray + // settings to the best of our abilities. + if (MOUSE_SELECTOR_MODE_INFOS[mode] === undefined) { + mode = undefined; + } + + // Restoring settings against unsupported modes should just go back to the + // default mode. + if ((mode & this.supportedModeMask_) === 0) { + mode = undefined; + } + + if (!mode) mode = this.defaultMode_; + this.mode = mode; + + const pos = tr.b.Settings.get(this.settingsKey_ + '.pos', undefined); + if (pos) this.pos = pos; + }, + + get supportedModeMask() { + return this.supportedModeMask_; + }, + + /** + * Sets the supported modes. Should be an OR-ing of MOUSE_SELECTOR_MODE + * values. + */ + set supportedModeMask(supportedModeMask) { + if (this.mode && (supportedModeMask & this.mode) === 0) { + throw new Error('supportedModeMask must include current mode.'); + } + + function createButtonForMode(mode) { + return button; + } + + this.supportedModeMask_ = supportedModeMask; + Polymer.dom(this.buttonsEl_).textContent = ''; + for (const modeName in MOUSE_SELECTOR_MODE) { + if (modeName === 'ALL_MODES') continue; + + const mode = MOUSE_SELECTOR_MODE[modeName]; + if ((this.supportedModeMask_ & mode) === 0) continue; + + const button = document.createElement('tr-ui-b-mouse-mode-icon'); + button.mode = mode; + Polymer.dom(button).classList.add('tool-button'); + + Polymer.dom(this.buttonsEl_).appendChild(button); + } + }, + + getButtonForMode_(mode) { + for (let i = 0; i < this.buttonsEl_.children.length; i++) { + const buttonEl = this.buttonsEl_.children[i]; + if (buttonEl.mode === mode) { + return buttonEl; + } + } + return undefined; + }, + + get mode() { + return this.currentMode_; + }, + + set mode(newMode) { + if (newMode !== undefined) { + if (typeof newMode !== 'number') { + throw new Error('Mode must be a number'); + } + if ((newMode & this.supportedModeMask_) === 0) { + throw new Error('Cannot switch to this mode, it is not supported'); + } + if (MOUSE_SELECTOR_MODE_INFOS[newMode] === undefined) { + throw new Error('Unrecognized mode'); + } + } + + let modeInfo; + + if (this.currentMode_ === newMode) return; + + if (this.currentMode_) { + const buttonEl = this.getButtonForMode_(this.currentMode_); + if (buttonEl) buttonEl.active = false; + + // End event. + if (this.isInteracting_) { + const mouseEvent = this.createEvent_( + MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.end); + this.dispatchEvent(mouseEvent); + } + + // Exit event. + modeInfo = MOUSE_SELECTOR_MODE_INFOS[this.currentMode_]; + tr.b.dispatchSimpleEvent(this, modeInfo.eventNames.exit, true); + } + + this.currentMode_ = newMode; + + if (this.currentMode_) { + const buttonEl = this.getButtonForMode_(this.currentMode_); + if (buttonEl) buttonEl.active = true; + + // Entering a new mode resets mouse down pos. + this.mouseDownPos_.x = this.mousePos_.x; + this.mouseDownPos_.y = this.mousePos_.y; + + // Enter event. + modeInfo = MOUSE_SELECTOR_MODE_INFOS[this.currentMode_]; + if (!this.isInAlternativeMode_) { + tr.b.dispatchSimpleEvent(this, modeInfo.eventNames.enter, true); + } + + // Begin event. + if (this.isInteracting_) { + const mouseEvent = this.createEvent_( + MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.begin); + this.dispatchEvent(mouseEvent); + } + } + + if (this.settingsKey_ && !this.isInAlternativeMode_) { + tr.b.Settings.set(this.settingsKey_ + '.mode', this.mode); + } + }, + + setKeyCodeForMode(mode, keyCode) { + if ((mode & this.supportedModeMask_) === 0) { + throw new Error('Mode not supported'); + } + this.modeToKeyCodeMap_[mode] = keyCode; + + if (!this.buttonsEl_) return; + + const buttonEl = this.getButtonForMode_(mode); + if (buttonEl) { + buttonEl.acceleratorKey = String.fromCharCode(keyCode); + } + }, + + setCurrentMousePosFromEvent_(e) { + this.mousePos_.x = e.clientX; + this.mousePos_.y = e.clientY; + }, + + createEvent_(eventName, sourceEvent) { + const event = new tr.b.Event(eventName, true); + event.clientX = this.mousePos_.x; + event.clientY = this.mousePos_.y; + event.deltaX = this.mousePos_.x - this.mouseDownPos_.x; + event.deltaY = this.mousePos_.y - this.mouseDownPos_.y; + event.mouseDownX = this.mouseDownPos_.x; + event.mouseDownY = this.mouseDownPos_.y; + event.didPreventDefault = false; + event.preventDefault = function() { + event.didPreventDefault = true; + if (sourceEvent) { + sourceEvent.preventDefault(); + } + }; + event.stopPropagation = function() { + sourceEvent.stopPropagation(); + }; + event.stopImmediatePropagation = function() { + throw new Error('Not implemented'); + }; + return event; + }, + + onMouseDown_(e) { + if (e.button !== 0) return; + this.setCurrentMousePosFromEvent_(e); + const mouseEvent = this.createEvent_( + MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.begin, e); + if (this.mode === MOUSE_SELECTOR_MODE.SELECTION) { + mouseEvent.appendSelection = isCmdOrCtrlPressed(e); + } + this.dispatchEvent(mouseEvent); + this.isInteracting_ = true; + this.isClick_ = true; + tr.ui.b.trackMouseMovesUntilMouseUp(this.onMouseMove_, this.onMouseUp_); + }, + + onMouseMove_(e) { + this.setCurrentMousePosFromEvent_(e); + + const mouseEvent = this.createEvent_( + MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.update, e); + this.dispatchEvent(mouseEvent); + + if (this.isInteracting_) { + this.checkIsClick_(e); + } + }, + + onMouseUp_(e) { + if (e.button !== 0) return; + + const mouseEvent = this.createEvent_( + MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.end, e); + mouseEvent.isClick = this.isClick_; + this.dispatchEvent(mouseEvent); + + if (this.isClick_ && !mouseEvent.didPreventDefault) { + this.dispatchClickEvents_(e); + } + + this.isInteracting_ = false; + this.updateAlternativeModeState_(e); + }, + + onButtonMouseDown_(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + }, + + onButtonMouseUp_(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + }, + + onButtonPress_(e) { + this.modeBeforeAlternativeModeActivated_ = undefined; + this.mode = e.target.mode; + e.preventDefault(); + }, + + onKeyDown_(e) { + // Keys dispatched to INPUT elements still bubble, even when they're + // handled. So, skip any events that targeted the input element. + if (e.path[0].tagName === 'INPUT') return; + + if (e.keyCode === ' '.charCodeAt(0)) { + this.spacePressed_ = true; + } + this.updateAlternativeModeState_(e); + }, + + onKeyUp_(e) { + // Keys dispatched to INPUT elements still bubble, even when they're + // handled. So, skip any events that targeted the input element. + if (e.path[0].tagName === 'INPUT') return; + + if (e.keyCode === ' '.charCodeAt(0)) { + this.spacePressed_ = false; + } + + let didHandleKey = false; + for (const [modeStr, keyCode] of Object.entries(this.modeToKeyCodeMap_)) { + if (e.keyCode === keyCode) { + this.modeBeforeAlternativeModeActivated_ = undefined; + const mode = parseInt(modeStr); + this.mode = mode; + didHandleKey = true; + } + } + + if (didHandleKey) { + e.preventDefault(); + e.stopPropagation(); + return; + } + this.updateAlternativeModeState_(e); + }, + + updateAlternativeModeState_(e) { + const shiftPressed = e.shiftKey; + const spacePressed = this.spacePressed_; + const cmdOrCtrlPressed = isCmdOrCtrlPressed(e); + + // Figure out the new mode + const smm = this.supportedModeMask_; + let newMode; + let isNewModeAnAlternativeMode = false; + if (shiftPressed && + (this.modifierToModeMap_[MODIFIER.SHIFT] & smm) !== 0) { + newMode = this.modifierToModeMap_[MODIFIER.SHIFT]; + isNewModeAnAlternativeMode = true; + } else if (spacePressed && + (this.modifierToModeMap_[MODIFIER.SPACE] & smm) !== 0) { + newMode = this.modifierToModeMap_[MODIFIER.SPACE]; + isNewModeAnAlternativeMode = true; + } else if (cmdOrCtrlPressed && + (this.modifierToModeMap_[MODIFIER.CMD_OR_CTRL] & smm) !== 0) { + newMode = this.modifierToModeMap_[MODIFIER.CMD_OR_CTRL]; + isNewModeAnAlternativeMode = true; + } else { + // Go to the old mode, if there is one. + if (this.isInAlternativeMode_) { + newMode = this.modeBeforeAlternativeModeActivated_; + isNewModeAnAlternativeMode = false; + } else { + newMode = undefined; + } + } + + // Maybe a mode change isn't needed. + if (this.mode === newMode || newMode === undefined) return; + + // Okay, we're changing. + if (isNewModeAnAlternativeMode) { + this.modeBeforeAlternativeModeActivated_ = this.mode; + } + this.mode = newMode; + }, + + get isInAlternativeMode_() { + return !!this.modeBeforeAlternativeModeActivated_; + }, + + setModifierForAlternateMode(mode, modifier) { + this.modifierToModeMap_[modifier] = mode; + }, + + get pos() { + return { + x: parseInt(this.style.left), + y: parseInt(this.style.top) + }; + }, + + set pos(pos) { + pos = this.constrainPositionToBounds_(pos); + + this.style.left = pos.x + 'px'; + this.style.top = pos.y + 'px'; + + if (this.settingsKey_) { + tr.b.Settings.set(this.settingsKey_ + '.pos', this.pos); + } + }, + + constrainPositionToBounds_(pos) { + const parent = this.offsetParent || document.body; + const parentRect = tr.ui.b.windowRectForElement(parent); + + const top = 0; + const bottom = parentRect.height - this.offsetHeight; + const left = 0; + const right = parentRect.width - this.offsetWidth; + + const res = {}; + res.x = Math.max(pos.x, left); + res.x = Math.min(res.x, right); + + res.y = Math.max(pos.y, top); + res.y = Math.min(res.y, bottom); + return res; + }, + + onDragHandleMouseDown_(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + + const mouseDownPos = { + x: e.clientX - this.offsetLeft, + y: e.clientY - this.offsetTop + }; + tr.ui.b.trackMouseMovesUntilMouseUp(function(e) { + const pos = {}; + pos.x = e.clientX - mouseDownPos.x; + pos.y = e.clientY - mouseDownPos.y; + this.pos = pos; + }.bind(this)); + }, + + checkIsClick_(e) { + if (!this.isInteracting_ || !this.isClick_) return; + + const deltaX = this.mousePos_.x - this.mouseDownPos_.x; + const deltaY = this.mousePos_.y - this.mouseDownPos_.y; + const minDist = MIN_MOUSE_SELECTION_DISTANCE; + + if (deltaX * deltaX + deltaY * deltaY > minDist * minDist) { + this.isClick_ = false; + } + }, + + dispatchClickEvents_(e) { + if (!this.isClick_) return; + + const modeInfo = MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.SELECTION]; + const eventNames = modeInfo.eventNames; + + let mouseEvent = this.createEvent_(eventNames.begin); + mouseEvent.appendSelection = isCmdOrCtrlPressed(e); + this.dispatchEvent(mouseEvent); + + mouseEvent = this.createEvent_(eventNames.end); + this.dispatchEvent(mouseEvent); + } + }); + + return { + MIN_MOUSE_SELECTION_DISTANCE, + MODIFIER, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector_test.html new file mode 100644 index 00000000000..577d40dbae3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector_test.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const MOUSE_SELECTOR_MODE = tr.ui.b.MOUSE_SELECTOR_MODE; + test('instantiate', function() { + const sel = document.createElement('tr-ui-b-mouse-mode-selector'); + sel.supportedModeMask = + MOUSE_SELECTOR_MODE.SELECTION | + MOUSE_SELECTOR_MODE.PANSCAN; + this.addHTMLOutput(sel); + }); + + test('changeMaskWithUnsupportedMode', function() { + const sel = document.createElement('tr-ui-b-mouse-mode-selector'); + sel.mode = MOUSE_SELECTOR_MODE.SELECTION; + assert.throw(function() { + sel.supportedModeMask = MOUSE_SELECTOR_MODE.ZOOM; + }); + }); + + test('modePersists', function() { + const sel1 = document.createElement('tr-ui-b-mouse-mode-selector'); + sel1.defaultMode_ = MOUSE_SELECTOR_MODE.ZOOM; + sel1.settingsKey = 'foo'; + assert.strictEqual(sel1.mode, MOUSE_SELECTOR_MODE.ZOOM); + + sel1.mode = MOUSE_SELECTOR_MODE.PANSCAN; + + const sel2 = document.createElement('tr-ui-b-mouse-mode-selector'); + sel2.settingsKey = 'foo'; + assert.strictEqual(sel2.mode, MOUSE_SELECTOR_MODE.PANSCAN); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_modes.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_modes.html new file mode 100644 index 00000000000..8d68f0279cc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_modes.html @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const MOUSE_SELECTOR_MODE = {}; + MOUSE_SELECTOR_MODE.SELECTION = 0x1; + MOUSE_SELECTOR_MODE.PANSCAN = 0x2; + MOUSE_SELECTOR_MODE.ZOOM = 0x4; + MOUSE_SELECTOR_MODE.TIMING = 0x8; + MOUSE_SELECTOR_MODE.ROTATE = 0x10; + MOUSE_SELECTOR_MODE.ALL_MODES = 0x1F; + + const MOUSE_SELECTOR_MODE_INFOS = {}; + MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.PANSCAN] = { + name: 'PANSCAN', + mode: MOUSE_SELECTOR_MODE.PANSCAN, + title: 'pan', + eventNames: { + enter: 'enterpan', + begin: 'beginpan', + update: 'updatepan', + end: 'endpan', + exit: 'exitpan' + }, + activeBackgroundPosition: '-30px -10px', + defaultBackgroundPosition: '0 -10px' + }; + MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.SELECTION] = { + name: 'SELECTION', + mode: MOUSE_SELECTOR_MODE.SELECTION, + title: 'selection', + eventNames: { + enter: 'enterselection', + begin: 'beginselection', + update: 'updateselection', + end: 'endselection', + exit: 'exitselection' + }, + activeBackgroundPosition: '-30px -40px', + defaultBackgroundPosition: '0 -40px' + }; + + MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.ZOOM] = { + name: 'ZOOM', + mode: MOUSE_SELECTOR_MODE.ZOOM, + title: 'zoom', + eventNames: { + enter: 'enterzoom', + begin: 'beginzoom', + update: 'updatezoom', + end: 'endzoom', + exit: 'exitzoom' + }, + activeBackgroundPosition: '-30px -70px', + defaultBackgroundPosition: '0 -70px' + }; + MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.TIMING] = { + name: 'TIMING', + mode: MOUSE_SELECTOR_MODE.TIMING, + title: 'timing', + eventNames: { + enter: 'entertiming', + begin: 'begintiming', + update: 'updatetiming', + end: 'endtiming', + exit: 'exittiming' + }, + activeBackgroundPosition: '-30px -100px', + defaultBackgroundPosition: '0 -100px' + }; + MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.ROTATE] = { + name: 'ROTATE', + mode: MOUSE_SELECTOR_MODE.ROTATE, + title: 'rotate', + eventNames: { + enter: 'enterrotate', + begin: 'beginrotate', + update: 'updaterotate', + end: 'endrotate', + exit: 'exitrotate' + }, + activeBackgroundPosition: '-30px -130px', + defaultBackgroundPosition: '0 -130px' + }; + + return { + MOUSE_SELECTOR_MODE_INFOS, + MOUSE_SELECTOR_MODE, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_tracker.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_tracker.html new file mode 100644 index 00000000000..f71e0ba2dea --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_tracker.html @@ -0,0 +1,117 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> +<script> +'use strict'; + +/** + * @fileoverview A Mouse-event abtraction that waits for + * mousedown, then watches for subsequent mousemove events + * until the next mouseup event, then waits again. + * State changes are signaled with + * 'mouse-tracker-start' : mousedown and tracking + * 'mouse-tracker-move' : mouse move + * 'mouse-tracker-end' : mouseup and not tracking. + */ + +tr.exportTo('tr.ui.b', function() { + /** + * @constructor + * @param {HTMLElement} targetElement will recv events 'mouse-tracker-start', + * 'mouse-tracker-move', 'mouse-tracker-end'. + */ + function MouseTracker(opt_targetElement) { + this.onMouseDown_ = this.onMouseDown_.bind(this); + this.onMouseMove_ = this.onMouseMove_.bind(this); + this.onMouseUp_ = this.onMouseUp_.bind(this); + + this.targetElement = opt_targetElement; + } + + MouseTracker.prototype = { + + get targetElement() { + return this.targetElement_; + }, + + set targetElement(targetElement) { + if (this.targetElement_) { + this.targetElement_.removeEventListener('mousedown', this.onMouseDown_); + } + this.targetElement_ = targetElement; + if (this.targetElement_) { + this.targetElement_.addEventListener('mousedown', this.onMouseDown_); + } + }, + + onMouseDown_(e) { + if (e.button !== 0) return true; + + e = this.remakeEvent_(e, 'mouse-tracker-start'); + this.targetElement_.dispatchEvent(e); + document.addEventListener('mousemove', this.onMouseMove_); + document.addEventListener('mouseup', this.onMouseUp_); + this.targetElement_.addEventListener('blur', this.onMouseUp_); + this.savePreviousUserSelect_ = document.body.style['-webkit-user-select']; + document.body.style['-webkit-user-select'] = 'none'; + e.preventDefault(); + return true; + }, + + onMouseMove_(e) { + e = this.remakeEvent_(e, 'mouse-tracker-move'); + this.targetElement_.dispatchEvent(e); + }, + + onMouseUp_(e) { + document.removeEventListener('mousemove', this.onMouseMove_); + document.removeEventListener('mouseup', this.onMouseUp_); + this.targetElement_.removeEventListener('blur', this.onMouseUp_); + document.body.style['-webkit-user-select'] = + this.savePreviousUserSelect_; + e = this.remakeEvent_(e, 'mouse-tracker-end'); + this.targetElement_.dispatchEvent(e); + }, + + remakeEvent_(e, newType) { + const remade = new tr.b.Event(newType, true, true); + remade.x = e.x; + remade.y = e.y; + remade.offsetX = e.offsetX; + remade.offsetY = e.offsetY; + remade.clientX = e.clientX; + remade.clientY = e.clientY; + return remade; + } + + }; + + function trackMouseMovesUntilMouseUp(mouseMoveHandler, + opt_mouseUpHandler, opt_keyUpHandler) { + function cleanupAndDispatchToMouseUp(e) { + document.removeEventListener('mousemove', mouseMoveHandler); + if (opt_keyUpHandler) { + document.removeEventListener('keyup', opt_keyUpHandler); + } + document.removeEventListener('mouseup', cleanupAndDispatchToMouseUp); + if (opt_mouseUpHandler) { + opt_mouseUpHandler(e); + } + } + document.addEventListener('mousemove', mouseMoveHandler); + if (opt_keyUpHandler) { + document.addEventListener('keyup', opt_keyUpHandler); + } + document.addEventListener('mouseup', cleanupAndDispatchToMouseUp); + } + + return { + MouseTracker, + trackMouseMovesUntilMouseUp, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart.html new file mode 100644 index 00000000000..19bda4bcec8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/ui/base/bar_chart.html"> +<link rel="import" href="/tracing/ui/base/d3.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const NameBarChart = tr.ui.b.define('name-bar-chart', tr.ui.b.BarChart); + + const Y_AXIS_PADDING = 2; + + NameBarChart.prototype = { + __proto__: tr.ui.b.BarChart.prototype, + + getDataPointAtChartPoint_(chartPoint) { + return { + x: tr.ui.b.BarChart.prototype.getDataPointAtChartPoint_.call( + this, chartPoint).x, + y: parseInt(Math.floor( + (this.graphHeight - chartPoint.y) / this.barHeight)) + }; + }, + + getXForDatum_(datum, index) { + return index; + }, + + get yAxisWidth() { + if (this.data.length === 0) return 0; + return Y_AXIS_PADDING + tr.b.math.Statistics.max( + this.data_, d => tr.ui.b.getSVGTextSize(this, d.x).width); + }, + + get defaultGraphHeight() { + return (3 + this.textHeightPx_) * this.data.length; + }, + + updateYAxis_(yAxis) { + // Building the y-axis requires measuring text. + // If necessary, wait for this element to be displayed. + if (tr.ui.b.getSVGTextSize(this, 'test').width === 0) { + tr.b.requestAnimationFrame(() => this.updateYAxis_(yAxis)); + return; + } + + // When we can measure text, we're ready to build the y-axis. + yAxis.selectAll('*').remove(); + if (this.hideYAxis) return; + const nameTexts = yAxis.selectAll('text').data(this.data_); + nameTexts + .enter() + .append('text') + .attr('x', d => -( + tr.ui.b.getSVGTextSize(this, d.x).width + Y_AXIS_PADDING)) + .attr('y', (d, index) => this.verticalScale_(index)) + .text(d => d.x); + nameTexts.exit().remove(); + + let previousTop = undefined; + for (const text of nameTexts[0]) { + const bbox = text.getBBox(); + if ((previousTop === undefined) || + (previousTop > (bbox.y + bbox.height))) { + previousTop = bbox.y; + } else { + text.style.opacity = 0; + } + } + } + }; + + return { + NameBarChart, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart_test.html new file mode 100644 index 00000000000..771022a86d9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart_test.html @@ -0,0 +1,127 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/name_bar_chart.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiation_singleSeries', function() { + const chart = new tr.ui.b.NameBarChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', value: 100}, + {x: 'ball', value: 110}, + {x: 'cat', value: 100}, + {x: 'dog', value: 50} + ]; + }); + + test('undefined', function() { + const chart = new tr.ui.b.NameBarChart(); + assert.throws(function() { + chart.data = undefined; + }); + }); + + test('instantiation_twoSeries', function() { + const chart = new tr.ui.b.NameBarChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', alpha: 100, beta: 50}, + {x: 'ball', alpha: 110, beta: 75}, + {x: 'cat', alpha: 100, beta: 125}, + {x: 'dog', alpha: 50, beta: 125} + ]; + + const r = new tr.b.math.Range(); + r.addValue(20); + r.addValue(40); + chart.brushedRange = r; + }); + + test('instantiation_twoSparseSeriesWithFirstValueSparse', function() { + const chart = new tr.ui.b.NameBarChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', alpha: 20, beta: undefined}, + {x: 'ball', alpha: undefined, beta: 10}, + {x: 'cat', alpha: 10, beta: undefined}, + {x: 'dog', alpha: undefined, beta: 20}, + {x: 'echo', alpha: 30, beta: 30} + ]; + }); + + test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() { + const chart = new tr.ui.b.NameBarChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', alpha: 20, beta: 40}, + {x: 'ball', alpha: undefined, beta: 10}, + {x: 'cat', alpha: 10, beta: undefined}, + {x: 'dog', alpha: undefined, beta: 20}, + {x: 'echo', alpha: 30, beta: undefined} + ]; + }); + + test('instantiation_interactiveBrushing', function() { + const chart = new tr.ui.b.NameBarChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', value: 50}, + {x: 'ball', value: 60}, + {x: 'cat', value: 80}, + {x: 'dog', value: 20}, + {x: 'echo', value: 30}, + {x: 'fortune', value: 20}, + {x: 'gpu', value: 15}, + {x: 'happy', value: 20} + ]; + + let mouseDownIndex = undefined; + let currentMouseIndex = undefined; + + function updateBrushedRange() { + const r = new tr.b.math.Range(); + r.min = Math.max(0, Math.min(mouseDownIndex, currentMouseIndex)); + r.max = Math.min(chart.data.length, Math.max(mouseDownIndex, + currentMouseIndex) + 1); + chart.brushedRange = r; + } + + chart.addEventListener('item-mousedown', function(e) { + mouseDownIndex = e.index; + currentMouseIndex = e.index; + updateBrushedRange(); + }); + chart.addEventListener('item-mousemove', function(e) { + if (e.button === undefined) return; + + currentMouseIndex = e.index; + updateBrushedRange(); + }); + chart.addEventListener('item-mouseup', function(e) { + currentMouseIndex = e.index; + updateBrushedRange(); + }); + }); + + test('instantiation_hideXandYAxis', function() { + const chart = new tr.ui.b.NameBarChart(); + chart.hideXAxis = true; + chart.hideYAxis = true; + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', value: 100}, + {x: 'ball', value: 110}, + {x: 'cat', value: 100}, + {x: 'dog', value: 50} + ]; + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart.html new file mode 100644 index 00000000000..6c0144da7ce --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/column_chart.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const MIN_GUIDELINE_HEIGHT_PX = 3; + + const CHECKBOX_WIDTH_PX = 18; + + const NameColumnChart = tr.ui.b.define( + 'name-column-chart', tr.ui.b.ColumnChart); + + NameColumnChart.prototype = { + __proto__: tr.ui.b.ColumnChart.prototype, + + get xAxisHeight() { + // Add 5px for descenders because SVG draws text baselines at the + // specified y-coordinate. + return 5 + (this.textHeightPx_ * this.data_.length); + }, + + updateMargins_() { + super.updateMargins_(); + let xAxisTickOverhangPx = 0; + for (let i = 0; i < this.data_.length; ++i) { + const datum = this.data_[i]; + xAxisTickOverhangPx = Math.max(xAxisTickOverhangPx, + this.xScale_(i) + tr.ui.b.getSVGTextSize(this, datum.x).width - + this.graphWidth); + } + this.margin.right = Math.max(this.margin.right, xAxisTickOverhangPx); + }, + + getXForDatum_(datum, index) { + return index; + }, + + get xAxisTickOffset() { + return 0.5; + }, + + updateXAxis_(xAxis) { + xAxis.selectAll('*').remove(); + if (this.hideXAxis) return; + + // Draw the tick labels from |this.data_[*].x|. + // Lay them out so that the text doesn't overlap. + // They may overhang into |this.margin.right|. + const nameTexts = xAxis.selectAll('text') + .data(this.data_); + nameTexts + .enter() + .append('text') + .attr('transform', (d, index) => 'translate(0, ' + + this.textHeightPx_ * (this.data_.length - index) + ')') + .attr('x', (d, index) => this.xScale_(index)) + .attr('y', d => this.graphHeight) + .text(d => d.x); + nameTexts.exit().remove(); + + // Draw lines to guide the eye from bottom center of the column to the + // tick label. + const guideLines = xAxis.selectAll('line.guide').data(this.data_); + guideLines.enter() + .append('line') + .attr('x1', (d, index) => this.xScale_(index + this.xAxisTickOffset)) + .attr('x2', (d, index) => this.xScale_(index + this.xAxisTickOffset)) + .attr('y1', () => this.graphHeight) + .attr('y2', (d, index) => this.graphHeight + Math.max( + MIN_GUIDELINE_HEIGHT_PX, + (this.textHeightPx_ * (this.data_.length - index - 1)))); + } + }; + + return { + NameColumnChart, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart_test.html new file mode 100644 index 00000000000..5e78dd6aa76 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart_test.html @@ -0,0 +1,119 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/name_column_chart.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiation_singleSeries', function() { + this.addHTMLOutput(document.createTextNode( + 'There should be a capital "A" at the end of the string of "a"s.')); + const chart = new tr.ui.b.NameColumnChart(); + this.addHTMLOutput(chart); + // Make a x-axis tick label long enough that it would overhang past the + // right edge of the legend in order to test that updateMargins_ extends the + // right margin. + chart.data = [ + {x: 'a'.repeat(20) + 'A', value: 100}, + {x: 'b', value: 110}, + {x: 'c', value: 100}, + {x: 'd', value: 50} + ]; + }); + + test('undefined', function() { + const chart = new tr.ui.b.NameColumnChart(); + assert.throws(function() { + chart.data = undefined; + }); + }); + + test('instantiation_twoSeries', function() { + const chart = new tr.ui.b.NameColumnChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', alpha: 100, beta: 50}, + {x: 'ball', alpha: 110, beta: 75}, + {x: 'cat', alpha: 100, beta: 125}, + {x: 'dog', alpha: 50, beta: 125} + ]; + + const r = new tr.b.math.Range(); + r.addValue(20); + r.addValue(40); + chart.brushedRange = r; + }); + + test('instantiation_twoSparseSeriesWithFirstValueSparse', function() { + const chart = new tr.ui.b.NameColumnChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', alpha: 20, beta: undefined}, + {x: 'ball', alpha: undefined, beta: 10}, + {x: 'cat', alpha: 10, beta: undefined}, + {x: 'dog', alpha: undefined, beta: 20}, + {x: 'echo', alpha: 30, beta: 30} + ]; + }); + + test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() { + const chart = new tr.ui.b.NameColumnChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', alpha: 20, beta: 40}, + {x: 'ball', alpha: undefined, beta: 10}, + {x: 'cat', alpha: 10, beta: undefined}, + {x: 'dog', alpha: undefined, beta: 20}, + {x: 'echo', alpha: 30, beta: undefined} + ]; + }); + + test('instantiation_interactiveBrushing', function() { + const chart = new tr.ui.b.NameColumnChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', value: 50}, + {x: 'ball', value: 60}, + {x: 'cat', value: 80}, + {x: 'dog', value: 20}, + {x: 'echo', value: 30}, + {x: 'fortune', value: 20}, + {x: 'gpu', value: 15}, + {x: 'happy', value: 20} + ]; + + let mouseDownIndex = undefined; + let currentMouseIndex = undefined; + + function updateBrushedRange() { + const r = new tr.b.math.Range(); + r.min = Math.max(0, Math.min(mouseDownIndex, currentMouseIndex)); + r.max = Math.min(chart.data.length, + Math.max(mouseDownIndex, currentMouseIndex) + 1); + chart.brushedRange = r; + } + + chart.addEventListener('item-mousedown', function(e) { + mouseDownIndex = e.index; + currentMouseIndex = e.index; + updateBrushedRange(); + }); + chart.addEventListener('item-mousemove', function(e) { + if (e.button === undefined) return; + + currentMouseIndex = e.index; + updateBrushedRange(); + }); + chart.addEventListener('item-mouseup', function(e) { + currentMouseIndex = e.index; + updateBrushedRange(); + }); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart.html new file mode 100644 index 00000000000..572ba36e533 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/ui/base/line_chart.html"> +<link rel="import" href="/tracing/ui/base/name_column_chart.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const LineChart = tr.ui.b.LineChart; + + // @constructor + const NameLineChart = tr.ui.b.define('name-line-chart', LineChart); + + NameLineChart.prototype = { + __proto__: LineChart.prototype, + + getXForDatum_(datum, index) { + return index; + }, + + get xAxisHeight() { + // Add 5px for descenders because SVG draws text baselines at the + // specified y-coordinate. + return 5 + (this.textHeightPx_ * this.data_.length); + }, + + get xAxisTickOffset() { + return 0; + }, + + updateMargins_() { + tr.ui.b.NameColumnChart.prototype.updateMargins_.call(this); + }, + + updateXAxis_(xAxis) { + xAxis.selectAll('*').remove(); + if (this.hideXAxis) return; + + tr.ui.b.NameColumnChart.prototype.updateXAxis_.call(this, xAxis); + + const baseline = xAxis.selectAll('path').data([this]); + baseline.enter().append('line') + .attr('stroke', 'black') + .attr('x1', this.xScale_(0)) + .attr('x2', this.xScale_(this.data_.length - 1)) + .attr('y1', this.graphHeight) + .attr('y2', this.graphHeight); + baseline.exit().remove(); + } + }; + + return { + NameLineChart, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart_test.html new file mode 100644 index 00000000000..fa7388103ae --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart_test.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/name_line_chart.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiation_singleSeries', function() { + const chart = new tr.ui.b.NameLineChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', value: 100}, + {x: 'ball', value: 110}, + {x: 'cat', value: 100}, + {x: 'dog', value: 50} + ]; + }); + + test('undefined', function() { + const chart = new tr.ui.b.NameLineChart(); + assert.throws(function() { + chart.data = undefined; + }); + }); + + test('instantiation_twoSeries', function() { + const chart = new tr.ui.b.NameLineChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', alpha: 100, beta: 50}, + {x: 'ball', alpha: 110, beta: 75}, + {x: 'cat', alpha: 100, beta: 125}, + {x: 'dog', alpha: 50, beta: 125} + ]; + + const r = new tr.b.math.Range(); + r.addValue(20); + r.addValue(40); + chart.brushedRange = r; + }); + + test('instantiation_twoSparseSeriesWithFirstValueSparse', function() { + const chart = new tr.ui.b.NameLineChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', alpha: 20, beta: undefined}, + {x: 'ball', alpha: undefined, beta: 10}, + {x: 'cat', alpha: 10, beta: undefined}, + {x: 'dog', alpha: undefined, beta: 20}, + {x: 'echo', alpha: 30, beta: 30} + ]; + }); + + test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() { + const chart = new tr.ui.b.NameLineChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', alpha: 20, beta: 40}, + {x: 'ball', alpha: undefined, beta: 10}, + {x: 'cat', alpha: 10, beta: undefined}, + {x: 'dog', alpha: undefined, beta: 20}, + {x: 'echo', alpha: 30, beta: undefined} + ]; + }); + + test('instantiation_interactiveBrushing', function() { + const chart = new tr.ui.b.NameLineChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 'apple', value: 50}, + {x: 'ball', value: 60}, + {x: 'cat', value: 80}, + {x: 'dog', value: 20}, + {x: 'echo', value: 30}, + {x: 'fortune', value: 20}, + {x: 'gpu', value: 15}, + {x: 'happy', value: 20} + ]; + + let mouseDownIndex = undefined; + let currentMouseIndex = undefined; + + function updateBrushedRange() { + const r = new tr.b.math.Range(); + r.min = Math.max(0, Math.min(mouseDownIndex, currentMouseIndex)); + r.max = Math.min(chart.data.length, Math.max(mouseDownIndex, + currentMouseIndex) + 1); + chart.brushedRange = r; + } + + chart.addEventListener('item-mousedown', function(e) { + mouseDownIndex = e.index; + currentMouseIndex = e.index; + updateBrushedRange(); + }); + chart.addEventListener('item-mousemove', function(e) { + if (e.button === undefined) return; + currentMouseIndex = e.index; + updateBrushedRange(); + }); + chart.addEventListener('item-mouseup', function(e) { + currentMouseIndex = e.index; + updateBrushedRange(); + }); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/overlay.html b/chromium/third_party/catapult/tracing/tracing/ui/base/overlay.html new file mode 100644 index 00000000000..9b1014d63b4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/overlay.html @@ -0,0 +1,351 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/event.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> + +<template id="overlay-template"> + <style> + overlay-mask { + left: 0; + padding: 8px; + position: absolute; + top: 0; + z-index: 1000; + font-family: sans-serif; + -webkit-justify-content: center; + background: rgba(0, 0, 0, 0.8); + display: flex; + height: 100%; + left: 0; + position: fixed; + top: 0; + width: 100%; + } + overlay-mask:focus { + outline: none; + } + overlay-vertical-centering-container { + -webkit-justify-content: center; + flex-direction: column; + display: flex; + } + overlay-frame { + z-index: 1100; + background: rgb(255, 255, 255); + border: 1px solid #ccc; + margin: 75px; + display: flex; + flex-direction: column; + min-height: 0; + } + title-bar { + -webkit-align-items: center; + flex-direction: row; + border-bottom: 1px solid #ccc; + background-color: #ddd; + display: flex; + padding: 5px; + flex: 0 0 auto; + } + title { + display: inline; + font-weight: bold; + flex: 1 1 auto; + } + close-button { + -webkit-align-self: flex-end; + border: 1px solid #eee; + background-color: #999; + font-size: 10pt; + font-weight: bold; + padding: 2px; + text-align: center; + width: 16px; + } + close-button:hover { + background-color: #ddd; + border-color: black; + cursor: pointer; + } + overlay-content { + display: flex; + flex: 1 1 auto; + flex-direction: column; + overflow-y: auto; + padding: 10px; + min-width: 300px; + min-height: 0; + } + button-bar { + -webkit-align-items: baseline; + border-top: 1px solid #ccc; + display: flex; + flex: 0 0 auto; + flex-direction: row-reverse; + padding: 4px; + } + </style> + + <overlay-mask> + <overlay-vertical-centering-container> + <overlay-frame> + <title-bar> + <title></title> + <close-button>✕</close-button> + </title-bar> + <overlay-content> + <content></content> + </overlay-content> + <button-bar></button-bar> + </overlay-frame> + </overlay-vertical-centering-container> + </overlay-mask> +</template> + +<script> +'use strict'; + +/** + * @fileoverview Implements an element that is hidden by default, but + * when shown, dims and (attempts to) disable the main document. + * + * You can turn any div into an overlay. Note that while an + * overlay element is shown, its parent is changed. Hiding the overlay + * restores its original parentage. + * + */ +tr.exportTo('tr.ui.b', function() { + if (tr.isHeadless) return {}; + + const THIS_DOC = document.currentScript.ownerDocument; + + /** + * Creates a new overlay element. It will not be visible until shown. + * @constructor + * @extends {HTMLDivElement} + */ + const Overlay = tr.ui.b.define('overlay'); + + Overlay.prototype = { + __proto__: HTMLDivElement.prototype, + + /** + * Initializes the overlay element. + */ + decorate() { + Polymer.dom(this).classList.add('overlay'); + + this.parentEl_ = this.ownerDocument.body; + + this.visible_ = false; + this.userCanClose_ = true; + + this.onKeyDown_ = this.onKeyDown_.bind(this); + this.onClick_ = this.onClick_.bind(this); + this.onFocusIn_ = this.onFocusIn_.bind(this); + this.onDocumentClick_ = this.onDocumentClick_.bind(this); + this.onClose_ = this.onClose_.bind(this); + + this.addEventListener('visible-change', + tr.ui.b.Overlay.prototype.onVisibleChange_.bind(this), true); + + // Setup the shadow root + const createShadowRoot = this.createShadowRoot || + this.webkitCreateShadowRoot; + this.shadow_ = createShadowRoot.call(this); + Polymer.dom(this.shadow_).appendChild( + tr.ui.b.instantiateTemplate('#overlay-template', THIS_DOC)); + + this.closeBtn_ = Polymer.dom(this.shadow_).querySelector('close-button'); + this.closeBtn_.addEventListener('click', this.onClose_); + + Polymer.dom(this.shadow_) + .querySelector('overlay-frame') + .addEventListener('click', this.onClick_); + + this.observer_ = new WebKitMutationObserver( + this.didButtonBarMutate_.bind(this)); + this.observer_.observe( + Polymer.dom(this.shadow_).querySelector('button-bar'), + { childList: true }); + + // title is a variable on regular HTMLElements. However, we want to + // use it for something more useful. + Object.defineProperty( + this, 'title', { + get() { + return Polymer.dom(Polymer.dom(this.shadow_) + .querySelector('title')).textContent; + }, + set(title) { + Polymer.dom(Polymer.dom(this.shadow_).querySelector('title')) + .textContent = title; + } + }); + }, + + set userCanClose(userCanClose) { + this.userCanClose_ = userCanClose; + this.closeBtn_.style.display = + userCanClose ? 'block' : 'none'; + }, + + get buttons() { + return Polymer.dom(this.shadow_).querySelector('button-bar'); + }, + + get visible() { + return this.visible_; + }, + + set visible(newValue) { + if (this.visible_ === newValue) return; + + this.visible_ = newValue; + const e = new tr.b.Event('visible-change'); + this.dispatchEvent(e); + }, + + onVisibleChange_() { + this.visible_ ? this.show_() : this.hide_(); + }, + + show_() { + Polymer.dom(this.parentEl_).appendChild(this); + + if (this.userCanClose_) { + this.addEventListener('keydown', this.onKeyDown_.bind(this)); + this.addEventListener('click', this.onDocumentClick_.bind(this)); + this.closeBtn_.addEventListener('click', this.onClose_); + } + + this.parentEl_.addEventListener('focusin', this.onFocusIn_); + this.tabIndex = 0; + + // Focus the first thing we find that makes sense. (Skip the close button + // as it doesn't make sense as the first thing to focus.) + const elList = + Polymer.dom(this).querySelectorAll('button, input, list, select, a'); + if (elList.length > 0) { + if (elList[0] === this.closeBtn_) { + if (elList.length > 1) return elList[1].focus(); + } else { + return elList[0].focus(); + } + } + this.focus(); + }, + + hide_() { + Polymer.dom(this.parentEl_).removeChild(this); + + this.parentEl_.removeEventListener('focusin', this.onFocusIn_); + + if (this.closeBtn_) { + this.closeBtn_.removeEventListener('click', this.onClose_); + } + + document.removeEventListener('keydown', this.onKeyDown_); + document.removeEventListener('click', this.onDocumentClick_); + }, + + onClose_(e) { + this.visible = false; + if ((e.type !== 'keydown') || + (e.type === 'keydown' && e.keyCode === 27)) { + e.stopPropagation(); + } + e.preventDefault(); + tr.b.dispatchSimpleEvent(this, 'closeclick'); + }, + + onFocusIn_(e) { + // Prevent focus from leaving the overlay. + + let node = e.target; + while (node) { + if (node === this) { + // |this| contains |e.target|, so nothing needs to be done. Allow + // focus to move from |this| to |e.target|. + return; + } + node = node.parentNode; + } + + // |e.target| is outside of |this|, so focus |this|. + tr.b.timeout(0).then(() => this.focus()); + e.preventDefault(); + e.stopPropagation(); + }, + + didButtonBarMutate_(e) { + const hasButtons = this.buttons.children.length > 0; + if (hasButtons) { + Polymer.dom(this.shadow_).querySelector('button-bar').style.display = + undefined; + } else { + Polymer.dom(this.shadow_).querySelector('button-bar').style.display = + 'none'; + } + }, + + onKeyDown_(e) { + // Disallow shift-tab back to another element. + if (e.keyCode === 9 && // tab + e.shiftKey && + e.target === this) { + e.preventDefault(); + return; + } + + if (e.keyCode !== 27) return; // escape + + this.onClose_(e); + }, + + onClick_(e) { + e.stopPropagation(); + }, + + onDocumentClick_(e) { + if (!this.userCanClose_) return; + + this.onClose_(e); + } + }; + + Overlay.showError = function(msg, opt_err) { + const o = new Overlay(); + o.title = 'Error'; + Polymer.dom(o).textContent = msg; + if (opt_err) { + const e = tr.b.normalizeException(opt_err); + + const stackDiv = document.createElement('pre'); + Polymer.dom(stackDiv).textContent = e.stack; + stackDiv.style.paddingLeft = '8px'; + stackDiv.style.margin = 0; + Polymer.dom(o).appendChild(stackDiv); + } + const b = document.createElement('button'); + Polymer.dom(b).textContent = 'OK'; + b.addEventListener('click', function() { + o.visible = false; + }); + Polymer.dom(o.buttons).appendChild(b); + o.visible = true; + return o; + }; + + return { + Overlay, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/overlay_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/overlay_test.html new file mode 100644 index 00000000000..ac22b08aa6b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/overlay_test.html @@ -0,0 +1,118 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/overlay.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function addShowButtonForDialog(dlg) { + const btn = document.createElement('button'); + Polymer.dom(btn).textContent = 'Launch Overlay'; + btn.addEventListener('click', function(e) { + dlg.visible = true; + e.stopPropagation(); + }); + + this.addHTMLOutput(btn); + } + + function makeButton(title) { + const btn = document.createElement('button'); + Polymer.dom(btn).textContent = title; + return btn; + } + + function makeCloseButton(dlg) { + const btn = makeButton('close'); + btn.addEventListener('click', function(e) { + dlg.onClose_(e); + }); + return btn; + } + + test('instantiate', function() { + const dlg = new tr.ui.b.Overlay(); + Polymer.dom(dlg).classList.add('example-overlay'); + dlg.title = 'ExampleOverlay'; + Polymer.dom(dlg).innerHTML = 'hello'; + Polymer.dom(dlg.buttons).appendChild(makeButton('i am a button')); + Polymer.dom(dlg.buttons).appendChild(makeCloseButton(dlg)); + Polymer.dom(dlg.buttons).appendChild(tr.ui.b.createSpan( + {textContent: 'i am a span'})); + addShowButtonForDialog.call(this, dlg); + }); + + test('instantiate_noButtons', function() { + const dlg = new tr.ui.b.Overlay(); + Polymer.dom(dlg).classList.add('example-overlay'); + dlg.title = 'ExampleOverlay'; + Polymer.dom(dlg).innerHTML = 'hello'; + addShowButtonForDialog.call(this, dlg); + }); + + test('instantiate_disableUserClose', function() { + const dlg = new tr.ui.b.Overlay(); + Polymer.dom(dlg).classList.add('example-overlay'); + dlg.userCanClose = false; + dlg.title = 'Unclosable'; + Polymer.dom(dlg).innerHTML = 'This has no close X button.'; + Polymer.dom(dlg.buttons).appendChild(makeCloseButton(dlg)); + addShowButtonForDialog.call(this, dlg); + }); + + test('instantiateTall', function() { + const dlg = new tr.ui.b.Overlay(); + dlg.title = 'TallContent'; + const contentEl = document.createElement('div'); + contentEl.style.overflowY = 'auto'; + Polymer.dom(dlg).appendChild(contentEl); + + for (let i = 0; i < 1000; i++) { + const el = document.createElement('div'); + Polymer.dom(el).textContent = 'line ' + i; + Polymer.dom(contentEl).appendChild(el); + } + + + Polymer.dom(dlg.buttons).appendChild(makeButton('i am a button')); + addShowButtonForDialog.call(this, dlg); + }); + + test('instantiateTallWithManyDirectChildren', function() { + const dlg = new tr.ui.b.Overlay(); + dlg.title = 'TallContent'; + for (let i = 0; i < 100; i++) { + const el = document.createElement('div'); + el.style.webkitFlex = '1 0 auto'; + Polymer.dom(el).textContent = 'line ' + i; + Polymer.dom(dlg).appendChild(el); + } + + Polymer.dom(dlg.buttons).appendChild(makeButton('i am a button')); + addShowButtonForDialog.call(this, dlg); + }); + + test('closeclickEvent', function() { + const dlg = new tr.ui.b.Overlay(); + dlg.title = 'Test closeclick event'; + const closeBtn = makeCloseButton(dlg); + Polymer.dom(dlg.buttons).appendChild(closeBtn); + + let closeClicked = false; + dlg.addEventListener('closeclick', function() { + closeClicked = true; + }); + + dlg.visible = true; + return tr.b.timeout(60).then(() => { + closeBtn.click(); + assert.isTrue(closeClicked); + }); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_postload.html b/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_postload.html new file mode 100644 index 00000000000..8a8016142ab --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_postload.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<script> +'use strict'; + +if (!Polymer.Settings.useNativeShadow) { + tr.showPanic('Polymer error', 'base only works in shadow mode'); +} +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_preload.html b/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_preload.html new file mode 100644 index 00000000000..4c21ef991ca --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_preload.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<script> +'use strict'; + +// Force Polymer into native shadowDom mode +if (window.Polymer) { + throw new Error('Cannot proceed. Polymer already present.'); +} +window.Polymer = {}; +window.Polymer.dom = 'shadow'; +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/quad_stack_view.html b/chromium/third_party/catapult/tracing/tracing/ui/base/quad_stack_view.html new file mode 100644 index 00000000000..d3d91fa6c00 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/quad_stack_view.html @@ -0,0 +1,688 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/math/bbox2.html"> +<link rel="import" href="/tracing/base/math/math.html"> +<link rel="import" href="/tracing/base/math/quad.html"> +<link rel="import" href="/tracing/base/math/rect.html"> +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/ui/base/camera.html"> +<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html"> +<link rel="import" href="/tracing/ui/base/mouse_tracker.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> + +<template id="quad-stack-view-template"> + <style> + #chrome-left { + background-image: url('../images/chrome-left.png'); + display: none; + } + #chrome-mid { + background-image: url('../images/chrome-mid.png'); + display: none; + } + #chrome-right { + background-image: url('../images/chrome-right.png'); + display: none; + } + </style> + + <div id="header"></div> + <input id="stacking-distance-slider" type="range" min=1 max=400 step=1> + </input> + <div id="canvas-scroller"> + <canvas id="canvas"></canvas> + </div> + <img id="chrome-left"/> + <img id="chrome-mid"/> + <img id="chrome-right"/> +</template> + +<script> +'use strict'; + +/** + * @fileoverview QuadStackView controls the content and viewing angle a + * QuadStack. + */ +tr.exportTo('tr.ui.b', function() { + const THIS_DOC = document.currentScript.ownerDocument; + + const constants = {}; + constants.IMAGE_LOAD_RETRY_TIME_MS = 500; + constants.SUBDIVISION_MINIMUM = 1; + constants.SUBDIVISION_RECURSION_DEPTH = 3; + constants.SUBDIVISION_DEPTH_THRESHOLD = 100; + constants.FAR_PLANE_DISTANCE = 10000; + + // Care of bckenney@ via + // http://extremelysatisfactorytotalitarianism.com/blog/?p=2120 + function drawTexturedTriangle(ctx, img, p0, p1, p2, t0, t1, t2) { + const tmpP0 = [p0[0], p0[1]]; + const tmpP1 = [p1[0], p1[1]]; + const tmpP2 = [p2[0], p2[1]]; + const tmpT0 = [t0[0], t0[1]]; + const tmpT1 = [t1[0], t1[1]]; + const tmpT2 = [t2[0], t2[1]]; + + ctx.beginPath(); + ctx.moveTo(tmpP0[0], tmpP0[1]); + ctx.lineTo(tmpP1[0], tmpP1[1]); + ctx.lineTo(tmpP2[0], tmpP2[1]); + ctx.closePath(); + + tmpP1[0] -= tmpP0[0]; + tmpP1[1] -= tmpP0[1]; + tmpP2[0] -= tmpP0[0]; + tmpP2[1] -= tmpP0[1]; + + tmpT1[0] -= tmpT0[0]; + tmpT1[1] -= tmpT0[1]; + tmpT2[0] -= tmpT0[0]; + tmpT2[1] -= tmpT0[1]; + + const det = 1 / (tmpT1[0] * tmpT2[1] - tmpT2[0] * tmpT1[1]); + + // linear transformation + const a = (tmpT2[1] * tmpP1[0] - tmpT1[1] * tmpP2[0]) * det; + const b = (tmpT2[1] * tmpP1[1] - tmpT1[1] * tmpP2[1]) * det; + const c = (tmpT1[0] * tmpP2[0] - tmpT2[0] * tmpP1[0]) * det; + const d = (tmpT1[0] * tmpP2[1] - tmpT2[0] * tmpP1[1]) * det; + + // translation + const e = tmpP0[0] - a * tmpT0[0] - c * tmpT0[1]; + const f = tmpP0[1] - b * tmpT0[0] - d * tmpT0[1]; + + ctx.save(); + ctx.transform(a, b, c, d, e, f); + ctx.clip(); + ctx.drawImage(img, 0, 0); + ctx.restore(); + } + + function drawTriangleSub( + ctx, img, p0, p1, p2, t0, t1, t2, opt_recursionDepth) { + const depth = opt_recursionDepth || 0; + + // We may subdivide if we are not at the limit of recursion. + let subdivisionIndex = 0; + if (depth < constants.SUBDIVISION_MINIMUM) { + subdivisionIndex = 7; + } else if (depth < constants.SUBDIVISION_RECURSION_DEPTH) { + if (Math.abs(p0[2] - p1[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD) { + subdivisionIndex += 1; + } + if (Math.abs(p0[2] - p2[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD) { + subdivisionIndex += 2; + } + if (Math.abs(p1[2] - p2[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD) { + subdivisionIndex += 4; + } + } + + // These need to be created every time, since temporaries + // outside of the scope will be rewritten in recursion. + const p01 = vec4.create(); + const p02 = vec4.create(); + const p12 = vec4.create(); + const t01 = vec2.create(); + const t02 = vec2.create(); + const t12 = vec2.create(); + + // Calculate the position before w-divide. + for (let i = 0; i < 2; ++i) { + p0[i] *= p0[2]; + p1[i] *= p1[2]; + p2[i] *= p2[2]; + } + + // Interpolate the 3d position. + for (let i = 0; i < 4; ++i) { + p01[i] = (p0[i] + p1[i]) / 2; + p02[i] = (p0[i] + p2[i]) / 2; + p12[i] = (p1[i] + p2[i]) / 2; + } + + // Re-apply w-divide to the original points and the interpolated ones. + for (let i = 0; i < 2; ++i) { + p0[i] /= p0[2]; + p1[i] /= p1[2]; + p2[i] /= p2[2]; + + p01[i] /= p01[2]; + p02[i] /= p02[2]; + p12[i] /= p12[2]; + } + + // Interpolate the texture coordinates. + for (let i = 0; i < 2; ++i) { + t01[i] = (t0[i] + t1[i]) / 2; + t02[i] = (t0[i] + t2[i]) / 2; + t12[i] = (t1[i] + t2[i]) / 2; + } + + // Based on the index, we subdivide the triangle differently. + // Assuming the triangle is p0, p1, p2 and points between i j + // are represented as pij (that is, a point between p2 and p0 + // is p02, etc), then the new triangles are defined by + // the 3rd 4th and 5th arguments into the function. + switch (subdivisionIndex) { + case 1: + drawTriangleSub(ctx, img, p0, p01, p2, t0, t01, t2, depth + 1); + drawTriangleSub(ctx, img, p01, p1, p2, t01, t1, t2, depth + 1); + break; + case 2: + drawTriangleSub(ctx, img, p0, p1, p02, t0, t1, t02, depth + 1); + drawTriangleSub(ctx, img, p1, p02, p2, t1, t02, t2, depth + 1); + break; + case 3: + drawTriangleSub(ctx, img, p0, p01, p02, t0, t01, t02, depth + 1); + drawTriangleSub(ctx, img, p02, p01, p2, t02, t01, t2, depth + 1); + drawTriangleSub(ctx, img, p01, p1, p2, t01, t1, t2, depth + 1); + break; + case 4: + drawTriangleSub(ctx, img, p0, p12, p2, t0, t12, t2, depth + 1); + drawTriangleSub(ctx, img, p0, p1, p12, t0, t1, t12, depth + 1); + break; + case 5: + drawTriangleSub(ctx, img, p0, p01, p2, t0, t01, t2, depth + 1); + drawTriangleSub(ctx, img, p2, p01, p12, t2, t01, t12, depth + 1); + drawTriangleSub(ctx, img, p01, p1, p12, t01, t1, t12, depth + 1); + break; + case 6: + drawTriangleSub(ctx, img, p0, p12, p02, t0, t12, t02, depth + 1); + drawTriangleSub(ctx, img, p0, p1, p12, t0, t1, t12, depth + 1); + drawTriangleSub(ctx, img, p02, p12, p2, t02, t12, t2, depth + 1); + break; + case 7: + drawTriangleSub(ctx, img, p0, p01, p02, t0, t01, t02, depth + 1); + drawTriangleSub(ctx, img, p01, p12, p02, t01, t12, t02, depth + 1); + drawTriangleSub(ctx, img, p01, p1, p12, t01, t1, t12, depth + 1); + drawTriangleSub(ctx, img, p02, p12, p2, t02, t12, t2, depth + 1); + break; + default: + // In the 0 case and all other cases, we simply draw the triangle. + drawTexturedTriangle(ctx, img, p0, p1, p2, t0, t1, t2); + break; + } + } + + // Created to avoid creating garbage when doing bulk transforms. + const tmpVec4 = vec4.create(); + function transform(transformed, point, matrix, viewport) { + vec4.set(tmpVec4, point[0], point[1], 0, 1); + vec4.transformMat4(tmpVec4, tmpVec4, matrix); + + let w = tmpVec4[3]; + if (w < 1e-6) w = 1e-6; + + transformed[0] = ((tmpVec4[0] / w) + 1) * viewport.width / 2; + transformed[1] = ((tmpVec4[1] / w) + 1) * viewport.height / 2; + transformed[2] = w; + } + + function drawProjectedQuadBackgroundToContext( + quad, p1, p2, p3, p4, ctx, quadCanvas) { + if (quad.imageData) { + quadCanvas.width = quad.imageData.width; + quadCanvas.height = quad.imageData.height; + quadCanvas.getContext('2d').putImageData(quad.imageData, 0, 0); + const quadBBox = new tr.b.math.BBox2(); + quadBBox.addQuad(quad); + const iw = quadCanvas.width; + const ih = quadCanvas.height; + drawTriangleSub( + ctx, quadCanvas, + p1, p2, p4, + [0, 0], [iw, 0], [0, ih]); + drawTriangleSub( + ctx, quadCanvas, + p2, p3, p4, + [iw, 0], [iw, ih], [0, ih]); + } + + if (quad.backgroundColor) { + ctx.fillStyle = quad.backgroundColor; + ctx.beginPath(); + ctx.moveTo(p1[0], p1[1]); + ctx.lineTo(p2[0], p2[1]); + ctx.lineTo(p3[0], p3[1]); + ctx.lineTo(p4[0], p4[1]); + ctx.closePath(); + ctx.fill(); + } + } + + function drawProjectedQuadOutlineToContext( + quad, p1, p2, p3, p4, ctx, quadCanvas) { + ctx.beginPath(); + ctx.moveTo(p1[0], p1[1]); + ctx.lineTo(p2[0], p2[1]); + ctx.lineTo(p3[0], p3[1]); + ctx.lineTo(p4[0], p4[1]); + ctx.closePath(); + ctx.save(); + if (quad.borderColor) { + ctx.strokeStyle = quad.borderColor; + } else { + ctx.strokeStyle = 'rgb(128,128,128)'; + } + + if (quad.shadowOffset) { + ctx.shadowColor = 'rgb(0, 0, 0)'; + ctx.shadowOffsetX = quad.shadowOffset[0]; + ctx.shadowOffsetY = quad.shadowOffset[1]; + if (quad.shadowBlur) { + ctx.shadowBlur = quad.shadowBlur; + } + } + + if (quad.borderWidth) { + ctx.lineWidth = quad.borderWidth; + } else { + ctx.lineWidth = 1; + } + + ctx.stroke(); + ctx.restore(); + } + + function drawProjectedQuadSelectionOutlineToContext( + quad, p1, p2, p3, p4, ctx, quadCanvas) { + if (!quad.upperBorderColor) return; + + ctx.lineWidth = 8; + ctx.strokeStyle = quad.upperBorderColor; + + ctx.beginPath(); + ctx.moveTo(p1[0], p1[1]); + ctx.lineTo(p2[0], p2[1]); + ctx.lineTo(p3[0], p3[1]); + ctx.lineTo(p4[0], p4[1]); + ctx.closePath(); + ctx.stroke(); + } + + function drawProjectedQuadToContext( + passNumber, quad, p1, p2, p3, p4, ctx, quadCanvas) { + if (passNumber === 0) { + drawProjectedQuadBackgroundToContext( + quad, p1, p2, p3, p4, ctx, quadCanvas); + } else if (passNumber === 1) { + drawProjectedQuadOutlineToContext( + quad, p1, p2, p3, p4, ctx, quadCanvas); + } else if (passNumber === 2) { + drawProjectedQuadSelectionOutlineToContext( + quad, p1, p2, p3, p4, ctx, quadCanvas); + } else { + throw new Error('Invalid pass number'); + } + } + + const tmpP1 = vec3.create(); + const tmpP2 = vec3.create(); + const tmpP3 = vec3.create(); + const tmpP4 = vec3.create(); + function transformAndProcessQuads( + matrix, viewport, quads, numPasses, handleQuadFunc, opt_arg1, opt_arg2) { + for (let passNumber = 0; passNumber < numPasses; passNumber++) { + for (let i = 0; i < quads.length; i++) { + const quad = quads[i]; + transform(tmpP1, quad.p1, matrix, viewport); + transform(tmpP2, quad.p2, matrix, viewport); + transform(tmpP3, quad.p3, matrix, viewport); + transform(tmpP4, quad.p4, matrix, viewport); + handleQuadFunc(passNumber, quad, + tmpP1, tmpP2, tmpP3, tmpP4, + opt_arg1, opt_arg2); + } + } + } + + /** + * @constructor + */ + const QuadStackView = tr.ui.b.define('quad-stack-view'); + + QuadStackView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.className = 'quad-stack-view'; + this.style.display = 'flex'; + this.style.position = 'relative'; + + const node = tr.ui.b.instantiateTemplate('#quad-stack-view-template', + THIS_DOC); + Polymer.dom(this).appendChild(node); + this.updateHeaderVisibility_(); + const header = Polymer.dom(this).querySelector('#header'); + header.style.position = 'absolute'; + header.style.fontSize = '70%'; + header.style.top = '10px'; + header.style.left = '10px'; + header.style.right = '150px'; + + const scroller = Polymer.dom(this).querySelector('#canvas-scroller'); + scroller.style.flexGrow = 1; + scroller.style.flexShrink = 1; + scroller.style.flexBasis = 'auto'; + scroller.style.minWidth = 0; + scroller.style.minHeight = 0; + scroller.style.overflow = 'auto'; + + this.canvas_ = Polymer.dom(this).querySelector('#canvas'); + this.chromeImages_ = { + left: Polymer.dom(this).querySelector('#chrome-left'), + mid: Polymer.dom(this).querySelector('#chrome-mid'), + right: Polymer.dom(this).querySelector('#chrome-right') + }; + + const stackingDistanceSlider = Polymer.dom(this).querySelector( + '#stacking-distance-slider'); + stackingDistanceSlider.style.position = 'absolute'; + stackingDistanceSlider.style.fontSize = '70%'; + stackingDistanceSlider.style.top = '10px'; + stackingDistanceSlider.style.right = '10px'; + stackingDistanceSlider.value = tr.b.Settings.get( + 'quadStackView.stackingDistance', 45); + stackingDistanceSlider.addEventListener( + 'change', this.onStackingDistanceChange_.bind(this)); + stackingDistanceSlider.addEventListener( + 'input', this.onStackingDistanceChange_.bind(this)); + + this.trackMouse_(); + + this.camera_ = new tr.ui.b.Camera(this.mouseModeSelector_); + this.camera_.addEventListener('renderrequired', + this.onRenderRequired_.bind(this)); + this.cameraWasReset_ = false; + this.camera_.canvas = this.canvas_; + + this.viewportRect_ = tr.b.math.Rect.fromXYWH(0, 0, 0, 0); + + this.pixelRatio_ = window.devicePixelRatio || 1; + }, + + updateHeaderVisibility_() { + if (this.headerText) { + Polymer.dom(this).querySelector('#header').style.display = ''; + } else { + Polymer.dom(this).querySelector('#header').style.display = 'none'; + } + }, + + get headerText() { + return Polymer.dom(this).querySelector('#header').textContent; + }, + + set headerText(headerText) { + Polymer.dom(this).querySelector('#header').textContent = headerText; + this.updateHeaderVisibility_(); + }, + + onStackingDistanceChange_(e) { + tr.b.Settings.set('quadStackView.stackingDistance', + this.stackingDistance); + this.scheduleRender(); + e.stopPropagation(); + }, + + get stackingDistance() { + return Polymer.dom(this).querySelector('#stacking-distance-slider').value; + }, + + get mouseModeSelector() { + return this.mouseModeSelector_; + }, + + get camera() { + return this.camera_; + }, + + set quads(q) { + this.quads_ = q; + this.scheduleRender(); + }, + + set deviceRect(rect) { + if (!rect || rect.equalTo(this.deviceRect_)) return; + + this.deviceRect_ = rect; + this.camera_.deviceRect = rect; + this.chromeQuad_ = undefined; + }, + + resize() { + if (!this.offsetParent) return true; + + const width = parseInt(window.getComputedStyle(this.offsetParent).width); + const height = parseInt(window.getComputedStyle( + this.offsetParent).height); + const rect = tr.b.math.Rect.fromXYWH(0, 0, width, height); + + if (rect.equalTo(this.viewportRect_)) return false; + + this.viewportRect_ = rect; + this.canvas_.style.width = width + 'px'; + this.canvas_.style.height = height + 'px'; + this.canvas_.width = this.pixelRatio_ * width; + this.canvas_.height = this.pixelRatio_ * height; + if (!this.cameraWasReset_) { + this.camera_.resetCamera(); + this.cameraWasReset_ = true; + } + return true; + }, + + readyToDraw() { + // If src isn't set yet, set it to ensure we can use + // the image to draw onto a canvas. + if (!this.chromeImages_.left.src) { + let leftContent = + window.getComputedStyle(this.chromeImages_.left).backgroundImage; + leftContent = tr.ui.b.extractUrlString(leftContent); + + let midContent = + window.getComputedStyle(this.chromeImages_.mid).backgroundImage; + midContent = tr.ui.b.extractUrlString(midContent); + + let rightContent = + window.getComputedStyle(this.chromeImages_.right).backgroundImage; + rightContent = tr.ui.b.extractUrlString(rightContent); + + this.chromeImages_.left.src = leftContent; + this.chromeImages_.mid.src = midContent; + this.chromeImages_.right.src = rightContent; + } + + // If all of the images are loaded (height > 0), then + // we are ready to draw. + return (this.chromeImages_.left.height > 0) && + (this.chromeImages_.mid.height > 0) && + (this.chromeImages_.right.height > 0); + }, + + get chromeQuad() { + if (this.chromeQuad_) return this.chromeQuad_; + + // Draw the chrome border into a separate canvas. + const chromeCanvas = document.createElement('canvas'); + const offsetY = this.chromeImages_.left.height; + + chromeCanvas.width = this.deviceRect_.width; + chromeCanvas.height = this.deviceRect_.height + offsetY; + + const leftWidth = this.chromeImages_.left.width; + const midWidth = this.chromeImages_.mid.width; + const rightWidth = this.chromeImages_.right.width; + + const chromeCtx = chromeCanvas.getContext('2d'); + chromeCtx.drawImage(this.chromeImages_.left, 0, 0); + + chromeCtx.save(); + chromeCtx.translate(leftWidth, 0); + + // Calculate the scale of the mid image. + const s = (this.deviceRect_.width - leftWidth - rightWidth) / midWidth; + chromeCtx.scale(s, 1); + + chromeCtx.drawImage(this.chromeImages_.mid, 0, 0); + chromeCtx.restore(); + + chromeCtx.drawImage( + this.chromeImages_.right, leftWidth + s * midWidth, 0); + + // Construct the quad. + const chromeRect = tr.b.math.Rect.fromXYWH( + this.deviceRect_.x, + this.deviceRect_.y - offsetY, + this.deviceRect_.width, + this.deviceRect_.height + offsetY); + const chromeQuad = tr.b.math.Quad.fromRect(chromeRect); + chromeQuad.stackingGroupId = this.maxStackingGroupId_ + 1; + chromeQuad.imageData = chromeCtx.getImageData( + 0, 0, chromeCanvas.width, chromeCanvas.height); + chromeQuad.shadowOffset = [0, 0]; + chromeQuad.shadowBlur = 5; + chromeQuad.borderWidth = 3; + this.chromeQuad_ = chromeQuad; + return this.chromeQuad_; + }, + + scheduleRender() { + if (this.redrawScheduled_) return false; + this.redrawScheduled_ = true; + tr.b.requestAnimationFrame(this.render, this); + }, + + onRenderRequired_(e) { + this.scheduleRender(); + }, + + stackTransformAndProcessQuads_( + numPasses, handleQuadFunc, includeChromeQuad, opt_arg1, opt_arg2) { + const mv = this.camera_.modelViewMatrix; + const p = this.camera_.projectionMatrix; + + const viewport = tr.b.math.Rect.fromXYWH( + 0, 0, this.canvas_.width, this.canvas_.height); + + // Calculate the quad stacks. + const quadStacks = []; + for (let i = 0; i < this.quads_.length; ++i) { + const quad = this.quads_[i]; + const stackingId = quad.stackingGroupId || 0; + while (stackingId >= quadStacks.length) { + quadStacks.push([]); + } + + quadStacks[stackingId].push(quad); + } + + const mvp = mat4.create(); + this.maxStackingGroupId_ = quadStacks.length; + const effectiveStackingDistance = + this.stackingDistance * this.camera_.stackingDistanceDampening; + + // Draw the quad stacks, raising each subsequent level. + mat4.multiply(mvp, p, mv); + for (let i = 0; i < quadStacks.length; ++i) { + transformAndProcessQuads(mvp, viewport, quadStacks[i], + numPasses, handleQuadFunc, + opt_arg1, opt_arg2); + + mat4.translate(mv, mv, [0, 0, effectiveStackingDistance]); + mat4.multiply(mvp, p, mv); + } + + if (includeChromeQuad && this.deviceRect_) { + transformAndProcessQuads(mvp, viewport, [this.chromeQuad], + numPasses, drawProjectedQuadToContext, + opt_arg1, opt_arg2); + } + }, + + render() { + this.redrawScheduled_ = false; + + if (!this.readyToDraw()) { + setTimeout(this.scheduleRender.bind(this), + constants.IMAGE_LOAD_RETRY_TIME_MS); + return; + } + + if (!this.quads_) return; + + const canvasCtx = this.canvas_.getContext('2d'); + if (!this.resize()) { + canvasCtx.clearRect(0, 0, this.canvas_.width, this.canvas_.height); + } + + const quadCanvas = document.createElement('canvas'); + this.stackTransformAndProcessQuads_( + 3, drawProjectedQuadToContext, true, + canvasCtx, quadCanvas); + quadCanvas.width = 0; // Hack: Frees the quadCanvas' resources. + }, + + trackMouse_() { + this.mouseModeSelector_ = document.createElement( + 'tr-ui-b-mouse-mode-selector'); + this.mouseModeSelector_.targetElement = this.canvas_; + this.mouseModeSelector_.supportedModeMask = + tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION | + tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN | + tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM | + tr.ui.b.MOUSE_SELECTOR_MODE.ROTATE; + this.mouseModeSelector_.mode = tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN; + this.mouseModeSelector_.pos = {x: 0, y: 100}; + Polymer.dom(this).appendChild(this.mouseModeSelector_); + this.mouseModeSelector_.settingsKey = + 'quadStackView.mouseModeSelector'; + + this.mouseModeSelector_.setModifierForAlternateMode( + tr.ui.b.MOUSE_SELECTOR_MODE.ROTATE, tr.ui.b.MODIFIER.SHIFT); + this.mouseModeSelector_.setModifierForAlternateMode( + tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN, tr.ui.b.MODIFIER.SPACE); + this.mouseModeSelector_.setModifierForAlternateMode( + tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM, tr.ui.b.MODIFIER.CMD_OR_CTRL); + + this.mouseModeSelector_.addEventListener('updateselection', + this.onSelectionUpdate_.bind(this)); + this.mouseModeSelector_.addEventListener('endselection', + this.onSelectionUpdate_.bind(this)); + }, + + extractRelativeMousePosition_(e) { + const br = this.canvas_.getBoundingClientRect(); + return [ + this.pixelRatio_ * (e.clientX - this.canvas_.offsetLeft - br.left), + this.pixelRatio_ * (e.clientY - this.canvas_.offsetTop - br.top) + ]; + }, + + onSelectionUpdate_(e) { + const mousePos = this.extractRelativeMousePosition_(e); + const res = []; + function handleQuad(passNumber, quad, p1, p2, p3, p4) { + if (tr.b.math.pointInImplicitQuad(mousePos, p1, p2, p3, p4)) { + res.push(quad); + } + } + this.stackTransformAndProcessQuads_(1, handleQuad, false); + e = new tr.b.Event('selectionchange'); + e.quads = res; + this.dispatchEvent(e); + } + }; + + return { + QuadStackView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker.html new file mode 100644 index 00000000000..27edfb59152 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker.html @@ -0,0 +1,150 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/ui.html"> + +<dom-module id='tr-ui-b-radio-picker'> + <template> + <style> + :host([vertical]) #container { + flex-direction: column; + } + :host(:not[vertical]) #container { + flex-direction: row; + } + #container { + display: flex; + } + #container > div { + padding-left: 1em; + padding-bottom: 0.5em; + } + </style> + <div id="container"></div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-b-radio-picker', + + created() { + this.needsInit_ = true; + this.settingsKey_ = undefined; + this.isReady_ = false; + this.radioButtons_ = undefined; + // Keeping track of which key is selected. This member should only be set + // set inside select() method to make sure that logical state & the UI + // state is consistent. + this.selectedKey_ = undefined; + }, + + ready() { + this.isReady_ = true; + this.maybeInit_(); + this.maybeRenderRadioButtons_(); + }, + + get vertical() { + return this.getAttribute('vertical'); + }, + + set vertical(vertical) { + if (vertical) { + this.setAttribute('vertical', true); + } else { + this.removeAttribute('vertical'); + } + }, + + get settingsKey() { + return this.settingsKey_; + }, + + set settingsKey(settingsKey) { + if (!this.needsInit_) { + throw new Error('Already initialized.'); + } + this.settingsKey_ = settingsKey; + this.maybeInit_(); + }, + + maybeInit_() { + if (!this.needsInit_) return; + if (this.settingsKey_ === undefined) return; + this.needsInit_ = false; + this.select(tr.b.Settings.get(this.settingsKey_)); + }, + + set items(items) { + this.radioButtons_ = {}; + items.forEach(function(e) { + if (e.key in this.radioButtons_) { + throw new Error(e.key + ' already exists'); + } + const radioButton = document.createElement('div'); + const input = document.createElement('input'); + const label = document.createElement('label'); + input.type = 'radio'; + input.id = e.label; + input.addEventListener('click', function() { + this.select(e.key); + }.bind(this)); + Polymer.dom(label).innerHTML = e.label; + label.htmlFor = e.label; + label.style.display = 'inline'; + Polymer.dom(radioButton).appendChild(input); + Polymer.dom(radioButton).appendChild(label); + this.radioButtons_[e.key] = input; + }.bind(this)); + + this.maybeInit_(); + this.maybeRenderRadioButtons_(); + }, + + maybeRenderRadioButtons_() { + if (!this.isReady_) return; + if (this.radioButtons_ === undefined) return; + for (const key in this.radioButtons_) { + Polymer.dom(this.$.container).appendChild( + this.radioButtons_[key].parentElement); + } + if (this.selectedKey_ !== undefined) { + this.select(this.selectedKey_); + } + }, + + select(key) { + if (key === undefined || key === this.selectedKey_) { + return; + } + if (this.radioButtons_ === undefined) { + this.selectedKey_ = key; + return; + } + if (!(key in this.radioButtons_)) { + throw new Error(key + ' does not exists'); + } + // Unselect the previous radio, update the key & select the new one. + if (this.selectedKey_ !== undefined) { + this.radioButtons_[this.selectedKey_].checked = false; + } + this.selectedKey_ = key; + tr.b.Settings.set(this.settingsKey_, this.selectedKey_); + if (this.selectedKey_ !== undefined) { + this.radioButtons_[this.selectedKey_].checked = true; + } + + this.dispatchEvent(new tr.b.Event('change', false)); + }, + + get selectedKey() { + return this.selectedKey_; + }, +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker_test.html new file mode 100644 index 00000000000..292cfaa4c9b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker_test.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/radio_picker.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('basic', function() { + const rp = document.createElement('tr-ui-b-radio-picker'); + rp.items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Boeing', label: 'I want to fly'}, + {key: 'Submarine', label: 'I want to swim'} + ]; + this.addHTMLOutput(rp); + assert.strictEqual(rp.selectedKey, undefined); + rp.select('Toyota'); + assert.strictEqual(rp.selectedKey, 'Toyota'); + }); + + test('persistentState_setSelectedKeyAfterSettingItems', function() { + const items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Boeing', label: 'I want to fly'}, + {key: 'Submarine', label: 'I want to swim'} + ]; + const container1 = tr.ui.b.createDiv({textContent: 'Radio Picker One'}); + container1.style.border = 'solid'; + const rp = document.createElement('tr-ui-b-radio-picker'); + rp.items = items; + rp.settingsKey = 'radio-picker-test-one'; + Polymer.dom(container1).appendChild(rp); + this.addHTMLOutput(container1); + assert.strictEqual(rp.selectedKey, undefined); + rp.select('Toyota'); + assert.strictEqual(rp.selectedKey, 'Toyota'); + + const container2 = tr.ui.b.createDiv({ + textContent: 'Radio Picker Two (same settingKey as Radio Picker One)'}); + container2.style.border = 'solid'; + const rp2 = document.createElement('tr-ui-b-radio-picker'); + rp2.items = items; + rp2.settingsKey = 'radio-picker-test-one'; + Polymer.dom(container2).appendChild(rp2); + this.addHTMLOutput(container2); + + assert.strictEqual(rp2.selectedKey, 'Toyota'); + }); + + test('persistentState_setSelectedKeyBeforeSettingItems', function() { + const items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Boeing', label: 'I want to fly'}, + {key: 'Submarine', label: 'I want to swim'} + ]; + const container1 = tr.ui.b.createDiv({textContent: 'Radio Picker One'}); + container1.style.border = 'solid'; + const rp = document.createElement('tr-ui-b-radio-picker'); + rp.settingsKey = 'radio-picker-test-two'; + rp.items = items; + Polymer.dom(container1).appendChild(rp); + this.addHTMLOutput(container1); + assert.strictEqual(rp.selectedKey, undefined); + rp.select('Boeing'); + assert.strictEqual(rp.selectedKey, 'Boeing'); + + const container2 = tr.ui.b.createDiv({ + textContent: 'Radio Picker Two (same settingKey as Radio Picker One)'}); + container2.style.border = 'solid'; + const rp2 = document.createElement('tr-ui-b-radio-picker'); + rp2.settingsKey = 'radio-picker-test-two'; + Polymer.dom(container2).appendChild(rp2); + this.addHTMLOutput(container2); + rp2.items = items; + + assert.strictEqual(rp2.selectedKey, 'Boeing'); + }); + + test('changeEventFired', function() { + const items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Boeing', label: 'I want to fly'}, + {key: 'Submarine', label: 'I want to swim'} + ]; + const rp = document.createElement('tr-ui-b-radio-picker'); + rp.items = items; + this.addHTMLOutput(rp); + rp.select('Boeing'); + assert.strictEqual(rp.selectedKey, 'Boeing'); + let fired = false; + rp.addEventListener('change', function(e) { + fired = true; + assert.strictEqual('Toyota', e.target.selectedKey); + }); + rp.select('Toyota'); + assert.isTrue(fired); + }); + + test('verticalAttribute', function() { + const items = [ + {key: 'Toyota', label: 'I want to drive Toyota'}, + {key: 'Boeing', label: 'I want to fly'}, + {key: 'Submarine', label: 'I want to swim'} + ]; + const rp = document.createElement('tr-ui-b-radio-picker'); + rp.items = items; + this.addHTMLOutput(rp); + assert.isNull(rp.getAttribute('vertical')); + rp.vertical = true; + assert.strictEqual(rp.getAttribute('vertical'), 'true'); + rp.vertical = false; + assert.isNull(rp.getAttribute('vertical')); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart.html new file mode 100644 index 00000000000..38b292e2360 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/ui/base/chart_base_2d.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const ScatterChart = tr.ui.b.define('scatter-chart', tr.ui.b.ChartBase2D); + + // @constructor + ScatterChart.Dot = function(x, y, radius, color, breadcrumb) { + this.x = x; + this.y = y; + this.radius = radius; + this.color = color; + this.breadcrumb = breadcrumb; + }; + + ScatterChart.prototype = { + __proto__: tr.ui.b.ChartBase2D.prototype, + + decorate() { + super.decorate(); + this.brushedXRange_ = new tr.b.math.Range(); + this.brushedYRange_ = new tr.b.math.Range(); + }, + + get hideLegend() { + return true; + }, + + get defaultGraphHeight() { + return 100; + }, + + get defaultGraphWidth() { + return 100; + }, + + updateMargins_() { + super.updateMargins_(); + if (this.data.length === 0) return; + + const rightOverhangPx = tr.b.math.Statistics.max( + this.data, d => this.xScale_(d.x) + d.radius - this.graphWidth); + this.margin.right = Math.max(this.margin.right, rightOverhangPx); + + const topOverhangPx = tr.b.math.Statistics.max( + this.data, d => (this.graphHeight - this.yScale_(d.y)) + d.radius) - + this.graphHeight; + this.margin.top = Math.max(this.margin.top, topOverhangPx); + }, + + setBrushedRanges(xRange, yRange) { + this.brushedXRange_.reset(); + this.brushedYRange_.reset(); + this.brushedXRange_.addRange(xRange); + this.brushedYRange_.addRange(yRange); + this.updateContents_(); + }, + + updateBrushContents_(brushSel) { + brushSel.selectAll('*').remove(); + if (this.brushedXRange_.isEmpty || this.brushedYRange_.isEmpty) return; + + const brushRectsSel = brushSel.selectAll('rect').data([undefined]); + brushRectsSel.enter().append('rect') + .attr('x', () => this.xScale_(this.brushedXRange_.min)) + .attr('y', () => this.yScale_(this.brushedYRange_.max)) + .attr('width', () => this.xScale_(this.brushedXRange_.max) - + this.xScale_(this.brushedXRange_.min)) + .attr('height', () => this.yScale_(this.brushedYRange_.min) - + this.yScale_(this.brushedYRange_.max)); + brushRectsSel.exit().remove(); + }, + + setDataFromCallbacks(data, getX, getY, getRadius, getColor) { + this.data = data.map(d => new ScatterChart.Dot( + getX(d), getY(d), getRadius(d), getColor(d), d)); + }, + + isDatumFieldSeries_(fieldName) { + return fieldName === 'y'; + }, + + updateDataContents_(dataSel) { + dataSel.selectAll('*').remove(); + dataSel.selectAll('circle') + .data(this.data_) + .enter() + .append('circle') + .attr('cx', d => this.xScale_(d.x)) + .attr('cy', d => this.yScale_(d.y)) + .attr('r', d => d.radius) + .attr('fill', d => d.color); + } + }; + + return { + ScatterChart, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart_test.html new file mode 100644 index 00000000000..a223589a2ac --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart_test.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/scatter_chart.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiation_singleSeries', function() { + const chart = new tr.ui.b.ScatterChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, y: 100, radius: 2, color: 'red'}, + {x: 20, y: 110, radius: 20, color: 'blue'}, + {x: 30, y: 100, radius: 10, color: 'red'}, + {x: 40, y: 50, radius: 10, color: 'red'} + ]; + }); + + test('instantiation_interactiveBrushing', function() { + const chart = new tr.ui.b.ScatterChart(); + this.addHTMLOutput(chart); + chart.data = [ + {x: 10, y: 50, radius: 2, color: 'blue'}, + {x: 20, y: 60, radius: 3, color: 'red'}, + {x: 30, y: 80, radius: 4, color: 'orange'}, + {x: 40, y: 20, radius: 5, color: 'purple'}, + {x: 50, y: 30, radius: 6, color: 'yellow'}, + {x: 60, y: 20, radius: 7, color: 'green'}, + {x: 70, y: 15, radius: 8, color: 'blue'}, + {x: 80, y: 20, radius: 9, color: 'red'} + ]; + + let mouseDown = undefined; + + function updateBrushedRange(e) { + const xRange = new tr.b.math.Range(); + if (e.x !== mouseDown.x) { + xRange.addValue(mouseDown.x); + xRange.addValue(e.x); + } + const yRange = new tr.b.math.Range(); + if (e.y !== mouseDown.y) { + yRange.addValue(mouseDown.y); + yRange.addValue(e.y); + } + chart.setBrushedRanges(xRange, yRange); + } + + chart.addEventListener('item-mousedown', function(e) { + mouseDown = e; + }); + chart.addEventListener('item-mousemove', function(e) { + updateBrushedRange(e); + }); + chart.addEventListener('item-mouseup', function(e) { + updateBrushedRange(e); + mouseDown = undefined; + }); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view.html b/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view.html new file mode 100644 index 00000000000..a651fc5cc3d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view.html @@ -0,0 +1,273 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +<!-- +@fileoverview A view that allows the user to control which single tab is +displayed. + +We follow a fairly standard web convention of backing our tabs with hidden radio +buttons but visible radio button labels (the tabs themselves) which toggle the +input element when clicked. Using hidden radio buttons makes sense, as both tabs +and radio buttons are input elements that allow user selection through clicking +and limit users to having one option selected at a time. +--> +<dom-module id='tr-ui-b-tab-view'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + + #selection_description, #tabs { + font-size: 12px; + } + + #selection_description { + display: inline-block; + font-weight: bold; + margin: 9px 0px 4px 20px; + } + + #tabs { + flex: 0 0 auto; + border-top: 1px solid #8e8e8e; + border-bottom: 1px solid #8e8e8e; + background-color: #ececec; + overflow: hidden; + margin: 0; + } + + #tabs input[type=radio] { + display: none; + } + + #tabs tab label { + cursor: pointer; + display: inline-block; + border: 1px solid #ececec; + margin: 5px 0px 0px 15px; + padding: 3px 10px 3px 10px; + } + + #tabs tab label span { + font-weight: bold; + } + + #tabs:focus input[type=radio]:checked ~ label { + outline: dotted 1px #8e8e8e; + outline-offset: -2px; + } + + #tabs input[type=radio]:checked ~ label { + background-color: white; + border: 1px solid #8e8e8e; + border-bottom: 1px solid white; + } + + #subView { + flex: 1 1 auto; + min-width: 0; + display: flex; + } + + #subView > * { + flex: 1 1 auto; + min-width: 0; + } + </style> + <div id='tabs' hidden="[[tabsHidden]]"> + <label id=selection_description>[[label_]]</label> + <template is=dom-repeat items=[[subViews_]]> + <tab> + <input type=radio name=tabs id$=[[computeRadioId_(item)]] + on-change='onTabChanged_' + checked='[[isChecked_(item)]]'/> + <label for$=[[computeRadioId_(item)]]> + <template is=dom-if if=[[item.tabIcon]]> + <span style$='[[item.tabIcon.style]]'>[[item.tabIcon.text]]</span> + </template> + [[item.tabLabel]] + </label> + </tab> + </template> + </div> + <div id='subView'></div> + <slot> + </slot> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-b-tab-view', + + properties: { + label_: { + type: String, + value: () => '' + }, + selectedSubView_: Object, + subViews_: { + type: Array, + value: () => [] + }, + tabsHidden: { + type: Boolean, + value: false, + observer: 'tabsHiddenChanged_' + } + }, + + ready() { + this.$.tabs.addEventListener('keydown', this.onKeyDown_.bind(this), true); + this.updateFocusability_(); + }, + + set label(newLabel) { + this.set('label_', newLabel); + }, + + get tabs() { + return this.get('subViews_'); + }, + + get selectedSubView() { + return this.selectedSubView_; + }, + + set selectedSubView(subView) { + if (subView === this.selectedSubView_) return; + + if (this.selectedSubView_) { + Polymer.dom(this.$.subView).removeChild(this.selectedSubView_); + const oldInput = this.root.getElementById(this.computeRadioId_( + this.selectedSubView_)); + if (oldInput) { + oldInput.checked = false; + } + } + + this.set('selectedSubView_', subView); + + if (subView) { + Polymer.dom(this.$.subView).appendChild(subView); + const newInput = this.root.getElementById(this.computeRadioId_(subView)); + if (newInput) { + newInput.checked = true; + } + } + + this.fire('selected-tab-change'); + }, + + clearSubViews() { + this.splice('subViews_', 0, this.subViews_.length); + this.selectedSubView = undefined; + this.updateFocusability_(); + }, + + addSubView(subView) { + this.push('subViews_', subView); + if (!this.selectedSubView_) this.selectedSubView = subView; + + this.updateFocusability_(); + }, + + get subViews() { + return this.subViews_; + }, + + resetSubViews(subViews) { + this.splice('subViews_', 0, this.subViews_.length); + if (subViews.length) { + for (const subView of subViews) { + this.push('subViews_', subView); + } + this.selectedSubView = subViews[0]; + } else { + this.selectedSubView = undefined; + } + this.updateFocusability_(); + }, + + onTabChanged_(event) { + this.selectedSubView = event.model.item; + }, + + isChecked_(subView) { + return this.selectedSubView_ === subView; + }, + + tabsHiddenChanged_() { + this.updateFocusability_(); + }, + + onKeyDown_(e) { + if (this.tabsHidden) return; + + let keyHandled = false; + switch (e.keyCode) { + // Arrow left. + case 37: + keyHandled = this.selectPreviousTabIfPossible(); + break; + + // Arrow right. + case 39: + keyHandled = this.selectNextTabIfPossible(); + break; + } + + if (!keyHandled) return; + e.stopPropagation(); + e.preventDefault(); + }, + + selectNextTabIfPossible() { + return this.selectTabByOffsetIfPossible_(1); + }, + + selectPreviousTabIfPossible() { + return this.selectTabByOffsetIfPossible_(-1); + }, + + selectTabByOffsetIfPossible_(offset) { + if (!this.selectedSubView_) return false; + const currentIndex = this.subViews_.indexOf(this.selectedSubView_); + const newSubView = this.tabs[currentIndex + offset]; + if (!newSubView) return false; + this.selectedSubView = newSubView; + return true; + }, + + shouldBeFocusable_() { + return !this.tabsHidden && this.subViews_.length > 0; + }, + + updateFocusability_() { + if (this.shouldBeFocusable_()) { + Polymer.dom(this.$.tabs).setAttribute('tabindex', 0); + } else { + Polymer.dom(this.$.tabs).removeAttribute('tabindex'); + } + }, + + computeRadioId_(subView) { + // We can't just use the tagName as the radio's ID because there are + // instances where a single subview type can handle multiple event types, + // and thus might be present multiple times in a single tab view. In order + // to avoid the case where we might have two tabs with the same ID, we + // uniquify this ID by appending the tab's label with all spaces replaced + // by dashes (because spaces aren't allowed in HTML IDs). + return subView.tagName + '-' + subView.tabLabel.replace(/ /g, '-'); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view_test.html new file mode 100644 index 00000000000..d5e9c19e672 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view_test.html @@ -0,0 +1,160 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/power_series.html"> +<link rel="import" href="/tracing/ui/analysis/alert_sub_view.html"> +<link rel="import" href="/tracing/ui/analysis/multi_power_sample_sub_view.html"> +<link rel="import" href="/tracing/ui/base/tab_view.html"> + +<dom-module id='tr-ui-b-tab-view-test-non-sub-view'> + <template> + <div></div> + </template> +</dom-module> +<script> +'use strict'; + +const nonSubViewBehavior = {}; + +Polymer({ + is: 'tr-ui-b-tab-view-test-non-sub-view', + behaviors: [nonSubViewBehavior] +}); + +tr.b.unittest.testSuite(function() { + function createPowerSampleSubView() { + const model = tr.c.TestUtils.newModel(function(m) { + m.device.powerSeries = new tr.model.PowerSeries(m.device); + + m.device.vSyncTimestamps = [0]; + m.device.powerSeries.addPowerSample(1, 1); + m.device.powerSeries.addPowerSample(2, 2); + m.device.powerSeries.addPowerSample(3, 3); + m.device.powerSeries.addPowerSample(4, 2); + }); + + const subView = document.createElement( + 'tr-ui-a-multi-power-sample-sub-view'); + subView.selection = new tr.model.EventSet(model.device.powerSeries.samples); + subView.tabLabel = 'Power samples'; + return subView; + } + + function createAlertSubView() { + const slice = tr.c.TestUtils.newSliceEx( + {title: 'b', start: 0, duration: 0.002}); + const alertInfo = new tr.model.EventInfo( + 'Alert 1', 'Critical alert', + [{ + label: 'Example', + textContent: 'Example page', + href: 'http://www.example.com' + }]); + + const alert = new tr.model.Alert(alertInfo, 5, [slice]); + const subView = document.createElement('tr-ui-a-alert-sub-view'); + subView.selection = new tr.model.EventSet(alert); + subView.tabLabel = 'Alerts'; + subView.tabIcon = { text: '\u26A0', style: 'color: red;' }; + + return subView; + } + + test('instantiate_noTabs', function() { + const tabView = document.createElement('tr-ui-b-tab-view'); + tabView.label = 'No items selected.'; + this.addHTMLOutput(tabView); + }); + + test('instantiate_oneTab', function() { + const tabView = document.createElement('tr-ui-b-tab-view'); + tabView.label = '1 item selected.'; + tabView.addSubView(createPowerSampleSubView()); + this.addHTMLOutput(tabView); + }); + + test('instantiate_twoTabs', function() { + const tabView = document.createElement('tr-ui-b-tab-view'); + tabView.label = '3 items selected.'; + tabView.addSubView(createPowerSampleSubView()); + tabView.addSubView(createAlertSubView()); + this.addHTMLOutput(tabView); + }); + + test('clearSubViews_selectedSubViewNullAfter', function() { + const tabView = document.createElement('tr-ui-b-tab-view'); + tabView.label = '3 items selected.'; + tabView.addSubView(createPowerSampleSubView()); + tabView.addSubView(createAlertSubView()); + + tabView.clearSubViews(); + + assert.isUndefined(tabView.selectedSubView); + }); + + test('changeSelectedSubView', function() { + let selectedTabChangeEventCount = 0; + const tabView = document.createElement('tr-ui-b-tab-view'); + tabView.addEventListener('selected-tab-change', function() { + selectedTabChangeEventCount++; + }); + + assert.isUndefined(tabView.selectedSubView); + assert.strictEqual(selectedTabChangeEventCount, 0); + + const view1 = createPowerSampleSubView(); + tabView.addSubView(view1); + assert.strictEqual(tabView.selectedSubView, view1); + assert.strictEqual(selectedTabChangeEventCount, 1); + + const view2 = createAlertSubView(); + tabView.addSubView(view2); + assert.strictEqual(tabView.selectedSubView, view1); + assert.strictEqual(selectedTabChangeEventCount, 1); + + tabView.selectedSubView = view2; + assert.strictEqual(tabView.selectedSubView, view2); + assert.strictEqual(selectedTabChangeEventCount, 2); + }); + + // Regression test: https://github.com/catapult-project/catapult/issues/2754 + test('instantiate_twoTabsSwitch', function() { + const tabView = document.createElement('tr-ui-b-tab-view'); + tabView.label = '3 items selected.'; + tabView.addSubView(createPowerSampleSubView()); + tabView.addSubView(createAlertSubView()); + this.addHTMLOutput(tabView); + Polymer.dom.flush(); + + tabView.selectedSubView = tabView.tabs[1]; + Polymer.dom.flush(); + + const selectedLabel = tabView.$.tabs.querySelector(':checked ~ label'); + assert.isTrue(selectedLabel && selectedLabel.innerText.includes('Alerts')); + }); + + // Regression test: https://github.com/catapult-project/catapult/issues/2755 + test('instantiate_twoTabsSwitchAndChange', function() { + const tabView = document.createElement('tr-ui-b-tab-view'); + this.addHTMLOutput(tabView); + tabView.addSubView(createPowerSampleSubView()); + tabView.addSubView(createAlertSubView()); + Polymer.dom.flush(); + + tabView.$.tabs.querySelectorAll('label')[2].click(); + tabView.$.tabs.querySelectorAll('label')[1].click(); + tabView.clearSubViews(); + tabView.addSubView(createPowerSampleSubView()); + Polymer.dom.flush(); + + assert.isTrue(!!tabView.$.tabs.querySelector(':checked')); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/table.html b/chromium/third_party/catapult/tracing/tracing/ui/base/table.html new file mode 100644 index 00000000000..3d707fb4b87 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/table.html @@ -0,0 +1,1808 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> + +<!-- +@fileoverview A container that constructs a table-like container. +--> +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const TableFormat = {}; + + TableFormat.SelectionMode = { + // Selection disabled. + // Default highlight: none. + NONE: 0, + + // Row selection mode. + // Default highlight: dark row. + ROW: 1, + + // Cell selection mode. + // Default highlight: dark cell and light row. + CELL: 2 + }; + + TableFormat.HighlightStyle = { + // Highlight depends on the current selection mode. + DEFAULT: 0, + + // No highlight. + NONE: 1, + + // Light highlight. + LIGHT: 2, + + // Dark highlight. + DARK: 3 + }; + + TableFormat.ColumnAlignment = { + LEFT: 0 /* default */, + RIGHT: 1 + }; + + return { + TableFormat, + }; +}); +</script> + +<dom-module id="tr-ui-b-table"> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + + table { + flex: 1 1 auto; + align-self: stretch; + border-collapse: separate; + border-spacing: 0; + border-width: 0; + -webkit-user-select: initial; + } + + tr > td { + padding: 2px 4px 2px 4px; + vertical-align: top; + } + + table > tbody:focus { + outline: none; + } + table > tbody:focus[selection-mode="row"] > tr[selected], + table > tbody:focus[selection-mode="cell"] > tr > td[selected], + table > tbody:focus > tr.empty-row > td { + outline: 1px dotted #666666; + outline-offset: -1px; + } + + button.toggle-button { + height: 15px; + line-height: 60%; + vertical-align: middle; + width: 100%; + } + + button > * { + height: 15px; + vertical-align: middle; + } + + td.button-column { + width: 30px; + } + + table > thead > tr > td.sensitive:hover { + background-color: #fcfcfc; + } + + table > thead > tr > td { + font-weight: bold; + text-align: left; + + background-color: #eee; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + border-top: 1px solid #ffffff; + border-bottom: 1px solid #aaa; + } + + table > tfoot { + background-color: #eee; + font-weight: bold; + } + + /* Light row and cell highlight. */ + table > tbody[row-highlight-style="light"] > tr[selected], + table > tbody[cell-highlight-style="light"] > tr > td[selected] { + background-color: rgb(213, 236, 229); /* light turquoise */ + } + table > tbody[row-highlight-style="light"] > + tr:not(.empty-row):not([selected]):hover, + table > tbody[cell-highlight-style="light"] > + tr:not(.empty-row):not([selected]) > td:hover { + background-color: #f6f6f6; /* light grey */ + } + + /* Dark row and cell highlight. */ + table > tbody[row-highlight-style="dark"] > tr[selected], + table > tbody[cell-highlight-style="dark"] > tr > td[selected] { + background-color: rgb(103, 199, 165); /* turquoise */ + } + table > tbody[row-highlight-style="dark"] > + tr:not(.empty-row):not([selected]):hover, + table > tbody[cell-highlight-style="dark"] > + tr:not(.empty-row):not([selected]) > td:hover { + background-color: #e6e6e6; /* grey */ + } + table > tbody[row-highlight-style="dark"] > tr:hover[selected], + table > tbody[cell-highlight-style="dark"] > tr[selected] > td:hover { + background-color: rgb(171, 217, 202); /* semi-light turquoise */ + } + + table > colgroup > col[selected] { + background-color: #e6e6e6; /* grey */ + } + + table > tbody > tr.empty-row > td { + color: #666; + font-style: italic; + text-align: center; + } + + table > tbody.has-footer > tr:last-child > td { + border-bottom: 1px solid #aaa; + } + + table > tfoot > tr:first-child > td { + border-top: 1px solid #ffffff; + } + + :host([zebra]) table tbody tr:nth-child(even) { + background-color: #f4f4f4; + } + + expand-button { + -webkit-user-select: none; + cursor: pointer; + margin-right: 3px; + font-size: smaller; + height: 1rem; + } + + expand-button.button-expanded { + transform: rotate(90deg); + } + </style> + <table> + <colgroup id="cols"> + </colgroup> + <thead id="head"> + </thead> + <tbody id="body"> + </tbody> + <tfoot id="foot"> + </tfoot> + </table> + </template> +</dom-module> +<script> +'use strict'; +(function() { + const RIGHT_ARROW = String.fromCharCode(0x25b6); + const UNSORTED_ARROW = String.fromCharCode(0x25BF); + const ASCENDING_ARROW = String.fromCharCode(0x25B4); + const DESCENDING_ARROW = String.fromCharCode(0x25BE); + + const SelectionMode = tr.ui.b.TableFormat.SelectionMode; + const SelectionModeValues = new Set(Object.values(SelectionMode)); + const HighlightStyle = tr.ui.b.TableFormat.HighlightStyle; + const HighlightStyleValues = new Set(Object.values(HighlightStyle)); + const ColumnAlignment = tr.ui.b.TableFormat.ColumnAlignment; + const ColumnAlignmentValues = new Set(Object.values(ColumnAlignment)); + + Polymer({ + is: 'tr-ui-b-table', + + created() { + this.selectionMode_ = SelectionMode.NONE; + this.rowHighlightStyle_ = HighlightStyle.DEFAULT; + this.cellHighlightStyle_ = HighlightStyle.DEFAULT; + this.selectedTableRowInfo_ = undefined; + this.selectedColumnIndex_ = undefined; + + this.tableColumns_ = []; + this.tableRows_ = []; + this.tableRowsInfo_ = new WeakMap(); + this.tableFooterRows_ = []; + this.tableFooterRowsInfo_ = new WeakMap(); + this.sortColumnIndex_ = undefined; + this.sortDescending_ = false; + this.columnsWithExpandButtons_ = []; + this.headerCells_ = []; + this.showHeader_ = true; + this.emptyValue_ = undefined; + this.subRowsPropertyName_ = 'subRows'; + this.customizeTableRowCallback_ = undefined; + this.defaultExpansionStateCallback_ = undefined; + this.userCanModifySortOrder_ = true; + this.computedFontSizePx_ = undefined; + }, + + ready() { + this.$.body.addEventListener( + 'keydown', this.onKeyDown_.bind(this), true); + this.$.body.addEventListener( + 'focus', this.onFocus_.bind(this), true); + }, + + clear() { + this.selectionMode_ = SelectionMode.NONE; + this.rowHighlightStyle_ = HighlightStyle.DEFAULT; + this.cellHighlightStyle_ = HighlightStyle.DEFAULT; + this.selectedTableRowInfo_ = undefined; + this.selectedColumnIndex_ = undefined; + + Polymer.dom(this).textContent = ''; + this.tableColumns_ = []; + this.tableRows_ = []; + this.tableRowsInfo_ = new WeakMap(); + this.tableFooterRows_ = []; + this.tableFooterRowsInfo_ = new WeakMap(); + this.sortColumnIndex_ = undefined; + this.sortDescending_ = false; + this.columnsWithExpandButtons_ = []; + this.headerCells_ = []; + this.showHeader_ = true; + this.emptyValue_ = undefined; + this.subRowsPropertyName_ = 'subRows'; + this.defaultExpansionStateCallback_ = undefined; + this.userCanModifySortOrder_ = true; + }, + + set zebra(zebra) { + if (zebra) { + this.setAttribute('zebra', true); + } else { + this.removeAttribute('zebra'); + } + }, + + get zebra() { + return this.getAttribute('zebra'); + }, + + get showHeader() { + return this.showHeader_; + }, + + set showHeader(showHeader) { + this.showHeader_ = showHeader; + this.scheduleRebuildHeaders_(); + }, + + set subRowsPropertyName(name) { + this.subRowsPropertyName_ = name; + }, + + /** + * This callback will be called whenever a body row is built + * for a userRow that has subRows and does not have an explicit + * isExpanded field. + * The callback should return true if the row should be expanded, + * or false if the row should be collapsed. + * @param {function(userRow, parentUserRow): boolean} cb The callback. + */ + set defaultExpansionStateCallback(cb) { + this.defaultExpansionStateCallback_ = cb; + this.scheduleRebuildBody_(); + }, + + /** + * This callback will be called whenever a body row is built. + * The callback's return value is ignored. + * @param {function(userRow, trElement)} cb The callback. + */ + set customizeTableRowCallback(cb) { + this.customizeTableRowCallback_ = cb; + this.scheduleRebuildBody_(); + }, + + get emptyValue() { + return this.emptyValue_; + }, + + set emptyValue(emptyValue) { + const previousEmptyValue = this.emptyValue_; + this.emptyValue_ = emptyValue; + if (this.tableRows_.length === 0 && emptyValue !== previousEmptyValue) { + this.scheduleRebuildBody_(); + } + }, + + /** + * Data objects should have the following fields: + * mandatory: title, value + * optional: width {string}, cmp {function}, colSpan {number}, + * showExpandButtons {boolean}, + * align {tr.ui.b.TableFormat.ColumnAlignment} + * + * @param {Array} columns An array of data objects. + */ + set tableColumns(columns) { + // Figure out the columns with expand buttons... + let columnsWithExpandButtons = []; + for (let i = 0; i < columns.length; i++) { + if (columns[i].showExpandButtons) { + columnsWithExpandButtons.push(i); + } + } + if (columnsWithExpandButtons.length === 0) { + // First column if none have specified. + columnsWithExpandButtons = [0]; + } + + // Sanity check columns. + for (let i = 0; i < columns.length; i++) { + const colInfo = columns[i]; + if (colInfo.width === undefined) continue; + + const hasExpandButton = columnsWithExpandButtons.includes(i); + + const w = colInfo.width; + if (w) { + if (/\d+px/.test(w)) { + continue; + } else if (/\d+%/.test(w)) { + if (hasExpandButton) { + throw new Error('Columns cannot be %-sized and host ' + + ' an expand button'); + } + } else { + throw new Error('Unrecognized width string'); + } + } + } + + // Try to preserve the user's sort choice. + // This is a 'best-effort' attempt, for example we compare columns by + // thier titles which can be HTML nodes in which case we might consider + // them different even if they look the same to the user. + let sortIndex = undefined; + const currentSortColumn = this.tableColumns[this.sortColumnIndex_]; + if (currentSortColumn) { + for (const [i, column] of columns.entries()) { + if (currentSortColumn.title === column.title) { + sortIndex = i; + break; + } + } + } + + // Commit the change. + this.tableColumns_ = columns; + this.headerCells_ = []; + this.columnsWithExpandButtons_ = columnsWithExpandButtons; + this.scheduleRebuildHeaders_(); + this.sortColumnIndex = sortIndex; + + // Blow away the table rows, too. + this.tableRows = this.tableRows_; + }, + + get tableColumns() { + return this.tableColumns_; + }, + + /** + * @param {Array} rows An array of 'row' objects with the following + * fields: + * optional: subRows An array of objects that have the same 'row' + * structure. Set subRowsPropertyName to use an + * alternative field name. + */ + set tableRows(rows) { + this.selectedTableRowInfo_ = undefined; + this.selectedColumnIndex_ = undefined; + this.tableRows_ = rows; + this.tableRowsInfo_ = new WeakMap(); + this.scheduleRebuildBody_(); + }, + + get tableRows() { + return this.tableRows_; + }, + + set footerRows(rows) { + this.tableFooterRows_ = rows; + this.tableFooterRowsInfo_ = new WeakMap(); + this.scheduleRebuildFooter_(); + }, + + get footerRows() { + return this.tableFooterRows_; + }, + + get userCanModifySortOrder() { + return this.userCanModifySortOrder_; + }, + + set userCanModifySortOrder(userCanModifySortOrder) { + const newUserCanModifySortOrder = !!userCanModifySortOrder; + if (newUserCanModifySortOrder === this.userCanModifySortOrder_) { + return; + } + + this.userCanModifySortOrder_ = newUserCanModifySortOrder; + this.scheduleRebuildHeaders_(); + }, + + set sortColumnIndex(number) { + if (number === this.sortColumnIndex_) return; + + if (number !== undefined) { + if (this.tableColumns_.length <= number) { + throw new Error('Column number ' + number + ' is out of bounds.'); + } + if (!this.tableColumns_[number].cmp) { + throw new Error('Column ' + number + ' does not have a comparator.'); + } + } + + this.sortColumnIndex_ = number; + this.updateHeaderArrows_(); + this.scheduleRebuildBody_(); + this.dispatchSortingChangedEvent_(); + }, + + get sortColumnIndex() { + return this.sortColumnIndex_; + }, + + set sortDescending(value) { + const newValue = !!value; + + if (newValue !== this.sortDescending_) { + this.sortDescending_ = newValue; + this.updateHeaderArrows_(); + this.scheduleRebuildBody_(); + this.dispatchSortingChangedEvent_(); + } + }, + + get sortDescending() { + return this.sortDescending_; + }, + + updateHeaderArrows_() { + for (let i = 0; i < this.headerCells_.length; i++) { + const headerCell = this.headerCells_[i]; + const isColumnCurrentlySorted = i === this.sortColumnIndex_; + if (!this.tableColumns_[i].cmp || + (!this.userCanModifySortOrder_ && !isColumnCurrentlySorted)) { + headerCell.sideContent = ''; + continue; + } + if (!isColumnCurrentlySorted) { + headerCell.sideContent = UNSORTED_ARROW; + headerCell.sideContentDisabled = false; + continue; + } + headerCell.sideContent = this.sortDescending_ ? + DESCENDING_ARROW : ASCENDING_ARROW; + headerCell.sideContentDisabled = !this.userCanModifySortOrder_; + } + }, + + generateHeaderColumns_() { + const selectedTableColumnIndex = this.selectedTableColumnIndex; + Polymer.dom(this.$.cols).textContent = ''; + for (let i = 0; i < this.tableColumns_.length; ++i) { + const colElement = document.createElement('col'); + if (i === selectedTableColumnIndex) { + colElement.setAttribute('selected', true); + } + Polymer.dom(this.$.cols).appendChild(colElement); + } + + this.headerCells_ = []; + Polymer.dom(this.$.head).textContent = ''; + if (!this.showHeader_) return; + + const tr = this.appendNewElement_(this.$.head, 'tr'); + for (let i = 0; i < this.tableColumns_.length; i++) { + const td = this.appendNewElement_(tr, 'td'); + + const headerCell = document.createElement('tr-ui-b-table-header-cell'); + headerCell.column = this.tableColumns_[i]; + + // If the table can be sorted by this column and the user can modify + // the sort order, attach a tap callback to the column. + if (this.tableColumns_[i].cmp) { + const isColumnCurrentlySorted = i === this.sortColumnIndex_; + if (isColumnCurrentlySorted) { + headerCell.sideContent = this.sortDescending_ ? + DESCENDING_ARROW : ASCENDING_ARROW; + if (!this.userCanModifySortOrder_) { + headerCell.sideContentDisabled = true; + } + } + if (this.userCanModifySortOrder_) { + Polymer.dom(td).classList.add('sensitive'); + if (!isColumnCurrentlySorted) { + headerCell.sideContent = UNSORTED_ARROW; + } + headerCell.tapCallback = this.createSortCallback_(i); + } + } + + Polymer.dom(td).appendChild(headerCell); + this.headerCells_.push(headerCell); + } + }, + + applySizes_() { + if (this.tableRows_.length === 0 && !this.showHeader) return; + + let rowToRemoveSizing; + let rowToSize; + if (this.showHeader) { + rowToSize = Polymer.dom(this.$.head).children[0]; + rowToRemoveSizing = Polymer.dom(this.$.body).children[0]; + } else { + rowToSize = Polymer.dom(this.$.body).children[0]; + rowToRemoveSizing = Polymer.dom(this.$.head).children[0]; + } + for (let i = 0; i < this.tableColumns_.length; i++) { + if (rowToRemoveSizing && Polymer.dom(rowToRemoveSizing).children[i]) { + const tdToRemoveSizing = Polymer.dom(rowToRemoveSizing).children[i]; + tdToRemoveSizing.style.minWidth = ''; + tdToRemoveSizing.style.width = ''; + } + + // Apply sizing. + const td = Polymer.dom(rowToSize).children[i]; + + let delta; + if (this.columnsWithExpandButtons_.includes(i)) { + td.style.paddingLeft = this.basicIndentation_ + 'px'; + delta = this.basicIndentation_ + 'px'; + } else { + delta = undefined; + } + + function calc(base, delta) { + if (delta) { + return 'calc(' + base + ' - ' + delta + ')'; + } + return base; + } + + const w = this.tableColumns_[i].width; + if (w) { + if (/\d+px/.test(w)) { + td.style.minWidth = calc(w, delta); + } else if (/\d+%/.test(w)) { + td.style.width = w; + } else { + throw new Error('Unrecognized width string: ' + w); + } + } + } + }, + + createSortCallback_(columnNumber) { + return function() { + if (!this.userCanModifySortOrder_) return; + + const previousIndex = this.sortColumnIndex; + this.sortColumnIndex = columnNumber; + if (previousIndex !== columnNumber) { + this.sortDescending = false; + } else { + this.sortDescending = !this.sortDescending; + } + }.bind(this); + }, + + generateTableRowNodes_(tableSection, userRows, rowInfoMap, + indentation, lastAddedRow, + parentRowInfo) { + if (this.sortColumnIndex_ !== undefined && + tableSection === this.$.body) { + userRows = userRows.slice(); // Don't mess with the input data. + userRows.sort(function(rowA, rowB) { + let c = this.tableColumns_[this.sortColumnIndex_].cmp( + rowA, rowB); + if (this.sortDescending_) { + c = -c; + } + return c; + }.bind(this)); + } + + for (let i = 0; i < userRows.length; i++) { + const userRow = userRows[i]; + const rowInfo = this.getOrCreateRowInfoFor_(rowInfoMap, userRow, + parentRowInfo); + const htmlNode = this.getHTMLNodeForRowInfo_( + tableSection, rowInfo, rowInfoMap, indentation); + + if (lastAddedRow === undefined) { + // Put first into the table. + Polymer.dom(tableSection).insertBefore( + htmlNode, Polymer.dom(tableSection).firstChild); + } else { + // This is shorthand for insertAfter(htmlNode, lastAdded). + const nextSiblingOfLastAdded = Polymer.dom(lastAddedRow).nextSibling; + Polymer.dom(tableSection).insertBefore( + htmlNode, nextSiblingOfLastAdded); + } + + lastAddedRow = htmlNode; + if (!rowInfo.isExpanded) continue; + + // Append subrows now. + lastAddedRow = this.generateTableRowNodes_( + tableSection, userRow[this.subRowsPropertyName_], rowInfoMap, + indentation + 1, lastAddedRow, rowInfo); + } + return lastAddedRow; + }, + + getOrCreateRowInfoFor_(rowInfoMap, userRow, parentRowInfo) { + let rowInfo = undefined; + + if (rowInfoMap.has(userRow)) { + rowInfo = rowInfoMap.get(userRow); + } else { + rowInfo = { + userRow, + htmlNode: undefined, + parentRowInfo + }; + rowInfoMap.set(userRow, rowInfo); + } + + // Recompute isExpanded in case defaultExpansionStateCallback_ has + // changed. + rowInfo.isExpanded = this.getExpandedForUserRow_(userRow); + + return rowInfo; + }, + + customizeTableRow_(userRow, trElement) { + if (!this.customizeTableRowCallback_) return; + this.customizeTableRowCallback_(userRow, trElement); + }, + + get basicIndentation_() { + if (this.computedFontSizePx_ === undefined) { + this.computedFontSizePx_ = parseInt( + getComputedStyle(this).fontSize) || 16; + } + return this.computedFontSizePx_ - 2; + }, + + getHTMLNodeForRowInfo_(tableSection, rowInfo, + rowInfoMap, indentation) { + if (rowInfo.htmlNode) { + this.customizeTableRow_(rowInfo.userRow, rowInfo.htmlNode); + return rowInfo.htmlNode; + } + + const INDENT_SPACE = indentation * 16; + const INDENT_SPACE_NO_BUTTON = indentation * 16 + this.basicIndentation_; + const trElement = this.ownerDocument.createElement('tr'); + rowInfo.htmlNode = trElement; + rowInfo.indentation = indentation; + trElement.rowInfo = rowInfo; + this.customizeTableRow_(rowInfo.userRow, trElement); + + const isBodyRow = tableSection === this.$.body; + const isExpandableRow = rowInfo.userRow[this.subRowsPropertyName_] && + rowInfo.userRow[this.subRowsPropertyName_].length; + + for (let i = 0; i < this.tableColumns_.length;) { + const td = this.appendNewElement_(trElement, 'td'); + td.columnIndex = i; + + const column = this.tableColumns_[i]; + const value = column.value(rowInfo.userRow); + const colSpan = column.colSpan ? column.colSpan : 1; + td.style.colSpan = colSpan; + + switch (column.align) { + case undefined: + case ColumnAlignment.LEFT: + break; + + case ColumnAlignment.RIGHT: + td.style.textAlign = 'right'; + break; + + default: + throw new Error('Invalid alignment of column at index=' + i + + ': ' + column.align); + } + + if (this.doesColumnIndexSupportSelection(i)) { + Polymer.dom(td).classList.add('supports-selection'); + } + + if (this.columnsWithExpandButtons_.includes(i)) { + if (rowInfo.userRow[this.subRowsPropertyName_] && + rowInfo.userRow[this.subRowsPropertyName_].length > 0) { + td.style.paddingLeft = INDENT_SPACE + 'px'; + td.style.display = 'flex'; + const expandButton = this.appendNewElement_(td, 'expand-button'); + Polymer.dom(expandButton).textContent = RIGHT_ARROW; + if (rowInfo.isExpanded) { + Polymer.dom(expandButton).classList.add('button-expanded'); + } + } else { + td.style.paddingLeft = INDENT_SPACE_NO_BUTTON + 'px'; + } + } + + if (value !== undefined) { + Polymer.dom(td).appendChild( + tr.ui.b.asHTMLOrTextNode(value, this.ownerDocument)); + } + + td.addEventListener('click', function(i, clickEvent) { + // Prevent automatically focusing on the table upon clicking on the + // table. Explicitly focus on it when appropriate (upon clicking on a + // selectable row/cell) instead. + clickEvent.preventDefault(); + + if (!isBodyRow && !isExpandableRow) return; + + clickEvent.stopPropagation(); + + if (clickEvent.target.tagName === 'EXPAND-BUTTON') { + this.setExpandedForUserRow_( + tableSection, rowInfoMap, + rowInfo.userRow, !rowInfo.isExpanded); + return; + } + + // If the row/cell can be selected and it's not selected yet, + // select it. + if (isBodyRow && this.selectionMode_ !== SelectionMode.NONE) { + let shouldSelect = false; + let shouldFocus = false; + switch (this.selectionMode_) { + case SelectionMode.ROW: + shouldSelect = this.selectedTableRowInfo_ !== rowInfo; + shouldFocus = true; + break; + case SelectionMode.CELL: + if (this.doesColumnIndexSupportSelection(i)) { + shouldSelect = this.selectedTableRowInfo_ !== rowInfo || + this.selectedColumnIndex_ !== i; + shouldFocus = true; + } + break; + default: + throw new Error('Invalid selection mode ' + + this.selectionMode_); + } + if (shouldFocus) { + this.focus(); + } + if (shouldSelect) { + this.didTableRowInfoGetClicked_(rowInfo, i); + return; + } + } + + // Otherwise, if the row is expandable, expand/collapse it. + if (isExpandableRow) { + this.setExpandedForUserRow_(tableSection, rowInfoMap, + rowInfo.userRow, !rowInfo.isExpanded); + } + }.bind(this, i)); + + // Add a double-click handler for stepping into a row/cell (if + // applicable). + if (isBodyRow) { + td.addEventListener('dblclick', function(i, e) { + e.stopPropagation(); + this.dispatchStepIntoEvent_(rowInfo, i); + }.bind(this, i)); + } + + i += colSpan; + } + + return rowInfo.htmlNode; + }, + + removeSubNodes_(tableSection, rowInfo, rowInfoMap) { + if (rowInfo.userRow[this.subRowsPropertyName_] === undefined) return; + + for (let i = 0; + i < rowInfo.userRow[this.subRowsPropertyName_].length; i++) { + const subRow = rowInfo.userRow[this.subRowsPropertyName_][i]; + const subRowInfo = rowInfoMap.get(subRow); + if (!subRowInfo) continue; + + const subNode = subRowInfo.htmlNode; + if (subNode && Polymer.dom(subNode).parentNode === tableSection) { + Polymer.dom(tableSection).removeChild(subNode); + this.removeSubNodes_(tableSection, subRowInfo, rowInfoMap); + } + } + }, + + scheduleRebuildHeaders_() { + this.headerDirty_ = true; + this.scheduleRebuild_(); + }, + + scheduleRebuildBody_() { + this.bodyDirty_ = true; + this.scheduleRebuild_(); + }, + + scheduleRebuildFooter_() { + this.footerDirty_ = true; + this.scheduleRebuild_(); + }, + + scheduleRebuild_() { + if (this.rebuildPending_) return; + + this.rebuildPending_ = true; + setTimeout(function() { + this.rebuildPending_ = false; + this.rebuild(); + }.bind(this), 0); + }, + + rebuildIfNeeded_() { + this.rebuild(); + }, + + rebuild() { + const wasBodyOrHeaderDirty = this.headerDirty_ || this.bodyDirty_; + + if (this.headerDirty_) { + this.generateHeaderColumns_(); + this.headerDirty_ = false; + } + if (this.bodyDirty_) { + Polymer.dom(this.$.body).textContent = ''; + this.generateTableRowNodes_( + this.$.body, + this.tableRows_, this.tableRowsInfo_, 0, + undefined, undefined); + if (this.tableRows_.length === 0 && this.emptyValue_ !== undefined) { + const trElement = this.ownerDocument.createElement('tr'); + Polymer.dom(this.$.body).appendChild(trElement); + Polymer.dom(trElement).classList.add('empty-row'); + const td = this.ownerDocument.createElement('td'); + Polymer.dom(trElement).appendChild(td); + td.colSpan = this.tableColumns_.length; + const emptyValue = this.emptyValue_; + Polymer.dom(td).appendChild( + tr.ui.b.asHTMLOrTextNode(emptyValue, this.ownerDocument)); + } + this.bodyDirty_ = false; + } + + if (wasBodyOrHeaderDirty) this.applySizes_(); + + if (this.footerDirty_) { + Polymer.dom(this.$.foot).textContent = ''; + this.generateTableRowNodes_( + this.$.foot, + this.tableFooterRows_, this.tableFooterRowsInfo_, 0, + undefined, undefined); + if (this.tableFooterRowsInfo_.length) { + Polymer.dom(this.$.body).classList.add('has-footer'); + } else { + Polymer.dom(this.$.body).classList.remove('has-footer'); + } + this.footerDirty_ = false; + } + }, + + appendNewElement_(parent, tagName) { + const element = parent.ownerDocument.createElement(tagName); + Polymer.dom(parent).appendChild(element); + return element; + }, + + getExpandedForTableRow(userRow) { + this.rebuildIfNeeded_(); + const rowInfo = this.tableRowsInfo_.get(userRow); + if (rowInfo === undefined) { + throw new Error('Row has not been seen, must expand its parents'); + } + return rowInfo.isExpanded; + }, + + getExpandedForUserRow_(userRow) { + if (userRow[this.subRowsPropertyName_] === undefined) { + return false; + } + if (userRow[this.subRowsPropertyName_].length === 0) { + return false; + } + if (userRow.isExpanded) { + return true; + } + if ((userRow.isExpanded !== undefined) && + (userRow.isExpanded === false)) { + return false; + } + + const rowInfo = this.tableRowsInfo_.get(userRow); + if (rowInfo && rowInfo.isExpanded) { + return true; + } + + if (this.defaultExpansionStateCallback_ === undefined) { + return false; + } + + let parentUserRow = undefined; + if (rowInfo && rowInfo.parentRowInfo) { + parentUserRow = rowInfo.parentRowInfo.userRow; + } + + return this.defaultExpansionStateCallback_( + userRow, parentUserRow); + }, + + setExpandedForTableRow(userRow, expanded) { + this.rebuildIfNeeded_(); + const rowInfo = this.tableRowsInfo_.get(userRow); + if (rowInfo === undefined) { + throw new Error('Row has not been seen, must expand its parents'); + } + return this.setExpandedForUserRow_(this.$.body, this.tableRowsInfo_, + userRow, expanded); + }, + + setExpandedForUserRow_(tableSection, rowInfoMap, + userRow, expanded) { + this.rebuildIfNeeded_(); + + const rowInfo = rowInfoMap.get(userRow); + if (rowInfo === undefined) { + throw new Error('Row has not been seen, must expand its parents'); + } + + const wasExpanded = rowInfo.isExpanded; + + rowInfo.isExpanded = !!expanded; + // If no node, then nothing further needs doing. + if (rowInfo.htmlNode === undefined) return; + + // If its detached, then nothing needs doing. + if (rowInfo.htmlNode.parentElement !== tableSection) { + return; + } + + // Otherwise, rebuild. + const expandButton = + Polymer.dom(rowInfo.htmlNode).querySelector('expand-button'); + if (rowInfo.isExpanded) { + Polymer.dom(expandButton).classList.add('button-expanded'); + const lastAddedRow = rowInfo.htmlNode; + if (rowInfo.userRow[this.subRowsPropertyName_]) { + this.generateTableRowNodes_( + tableSection, + rowInfo.userRow[this.subRowsPropertyName_], rowInfoMap, + rowInfo.indentation + 1, + lastAddedRow, rowInfo); + } + } else { + Polymer.dom(expandButton).classList.remove('button-expanded'); + this.removeSubNodes_(tableSection, rowInfo, rowInfoMap); + } + + if (wasExpanded !== rowInfo.isExpanded) { + const e = new tr.b.Event('row-expanded-changed'); + e.row = rowInfo.userRow; + this.dispatchEvent(e); + } + + this.maybeUpdateSelectedRow_(); + }, + + get selectionMode() { + return this.selectionMode_; + }, + + set selectionMode(selectionMode) { + if (!SelectionModeValues.has(selectionMode)) { + throw new Error('Invalid selection mode ' + selectionMode); + } + this.rebuildIfNeeded_(); + this.selectionMode_ = selectionMode; + this.didSelectionStateChange_(); + }, + + get rowHighlightStyle() { + return this.rowHighlightStyle_; + }, + + set rowHighlightStyle(rowHighlightStyle) { + if (!HighlightStyleValues.has(rowHighlightStyle)) { + throw new Error('Invalid row highlight style ' + rowHighlightStyle); + } + this.rebuildIfNeeded_(); + this.rowHighlightStyle_ = rowHighlightStyle; + this.didSelectionStateChange_(); + }, + + get resolvedRowHighlightStyle() { + if (this.rowHighlightStyle_ !== HighlightStyle.DEFAULT) { + return this.rowHighlightStyle_; + } + switch (this.selectionMode_) { + case SelectionMode.NONE: + return HighlightStyle.NONE; + case SelectionMode.ROW: + return HighlightStyle.DARK; + case SelectionMode.CELL: + return HighlightStyle.LIGHT; + default: + throw new Error('Invalid selection mode ' + selectionMode); + } + }, + + get cellHighlightStyle() { + return this.cellHighlightStyle_; + }, + + set cellHighlightStyle(cellHighlightStyle) { + if (!HighlightStyleValues.has(cellHighlightStyle)) { + throw new Error('Invalid cell highlight style ' + cellHighlightStyle); + } + this.rebuildIfNeeded_(); + this.cellHighlightStyle_ = cellHighlightStyle; + this.didSelectionStateChange_(); + }, + + get resolvedCellHighlightStyle() { + if (this.cellHighlightStyle_ !== HighlightStyle.DEFAULT) { + return this.cellHighlightStyle_; + } + switch (this.selectionMode_) { + case SelectionMode.NONE: + case SelectionMode.ROW: + return HighlightStyle.NONE; + case SelectionMode.CELL: + return HighlightStyle.DARK; + default: + throw new Error('Invalid selection mode ' + selectionMode); + } + }, + + setHighlightStyle_(highlightAttribute, resolvedHighlightStyle) { + switch (resolvedHighlightStyle) { + case HighlightStyle.NONE: + Polymer.dom(this.$.body).removeAttribute(highlightAttribute); + break; + case HighlightStyle.LIGHT: + Polymer.dom(this.$.body).setAttribute(highlightAttribute, 'light'); + break; + case HighlightStyle.DARK: + Polymer.dom(this.$.body).setAttribute(highlightAttribute, 'dark'); + break; + default: + throw new Error('Invalid resolved highlight style ' + + resolvedHighlightStyle); + } + }, + + didSelectionStateChange_() { + this.setHighlightStyle_('row-highlight-style', + this.resolvedRowHighlightStyle); + this.setHighlightStyle_('cell-highlight-style', + this.resolvedCellHighlightStyle); + + this.removeSelectedState_(); + + switch (this.selectionMode_) { + case SelectionMode.ROW: + // TODO: Replace this.selectionMode_ with a proper Polymer attribute. + Polymer.dom(this.$.body).setAttribute('selection-mode', 'row'); + Polymer.dom(this.$.body).setAttribute('tabindex', 0); + this.selectedColumnIndex_ = undefined; + break; + case SelectionMode.CELL: + Polymer.dom(this.$.body).setAttribute('selection-mode', 'cell'); + Polymer.dom(this.$.body).setAttribute('tabindex', 0); + if (this.selectedTableRowInfo_ && + this.selectedColumnIndex_ === undefined) { + const i = this.getFirstSelectableColumnIndex_(); + if (i === -1) { + // No column is selectable. + this.selectedTableRowInfo_ = undefined; + } else { + this.selectedColumnIndex_ = i; + } + } + break; + case SelectionMode.NONE: + Polymer.dom(this.$.body).removeAttribute('selection-mode'); + Polymer.dom(this.$.body).removeAttribute('tabindex'); + this.$.body.blur(); // Remove focus (if applicable). + this.selectedTableRowInfo_ = undefined; + this.selectedColumnIndex_ = undefined; + break; + default: + throw new Error('Invalid selection mode ' + this.selectionMode_); + } + + this.maybeUpdateSelectedRow_(); + }, + + maybeUpdateSelectedRow_() { + if (this.selectedTableRowInfo_ === undefined) return; + + // selectedUserRow may not be visible + function isVisible(rowInfo) { + if (!rowInfo.htmlNode) return false; + return !!rowInfo.htmlNode.parentElement; + } + if (isVisible(this.selectedTableRowInfo_)) { + this.updateSelectedState_(); + return; + } + + this.removeSelectedState_(); + let curRowInfo = this.selectedTableRowInfo_; + while (curRowInfo && !isVisible(curRowInfo)) { + curRowInfo = curRowInfo.parentRowInfo; + } + + this.selectedTableRowInfo_ = curRowInfo; + if (this.selectedTableRowInfo_) { + this.updateSelectedState_(); + } else { + this.selectedColumnIndex_ = undefined; + } + }, + + didTableRowInfoGetClicked_(rowInfo, columnIndex) { + switch (this.selectionMode_) { + case SelectionMode.NONE: + return; + + case SelectionMode.CELL: + if (!this.doesColumnIndexSupportSelection(columnIndex)) { + return; + } + if (this.selectedColumnIndex !== columnIndex) { + this.selectedColumnIndex = columnIndex; + } + // Fall through. + + case SelectionMode.ROW: + if (this.selectedTableRowInfo_ !== rowInfo) { + this.selectedTableRow = rowInfo.userRow; + } + } + }, + + dispatchStepIntoEvent_(rowInfo, columnIndex) { + const e = new tr.b.Event('step-into'); + e.tableRow = rowInfo.userRow; + e.tableColumn = this.tableColumns_[columnIndex]; + e.columnIndex = columnIndex; + this.dispatchEvent(e); + }, + + /** + * If the selectionMode is CELL and a cell is selected, + * return an object containing the row, column, and value of the selected + * cell. + * + * @return {undefined|!Object} + */ + get selectedCell() { + const row = this.selectedTableRow; + const columnIndex = this.selectedColumnIndex; + if (row === undefined || columnIndex === undefined || + this.tableColumns_.length <= columnIndex) { + return undefined; + } + const column = this.tableColumns_[columnIndex]; + return { + row, + column, + value: column.value(row) + }; + }, + + /** + * If a column is selected, return the object describing the selected + * column. + * + * Columns can be selected independently of rows and cells. So it is + * possible to select column 0 and cell [0,0], or column 1 and cell [0,0], + * for example. See |selectedCell| for how to access the selected cell when + * the selectionMode is CELL. + * + * |selectedTableColumn| is entirely independent of |selectedColumnIndex|. + * When the table selectionMode is CELL, use |selectedTableRow| and + * |selectedColumnIndex| to find the selected cell. + * When one or more columns have |selectable:true|, then use + * |selectedTableColumn| to find the selected column, which may be either + * the same as or different from |selectedColumnIndex|, if a cell is also + * selected. + * + * @return {number|undefined} + */ + get selectedTableColumnIndex() { + const cols = Polymer.dom(this.$.cols).children; + for (let i = 0; i < cols.length; ++i) { + if (cols[i].getAttribute('selected')) { + return i; + } + } + return undefined; + }, + + /** + * @param {number|undefined} index + */ + set selectedTableColumnIndex(selectedIndex) { + const cols = Polymer.dom(this.$.cols).children; + for (let i = 0; i < cols.length; ++i) { + if (i === selectedIndex) { + cols[i].setAttribute('selected', true); + } else { + cols[i].removeAttribute('selected'); + } + } + }, + + get selectedTableRow() { + if (!this.selectedTableRowInfo_) return undefined; + return this.selectedTableRowInfo_.userRow; + }, + + set selectedTableRow(userRow) { + this.rebuildIfNeeded_(); + if (this.selectionMode_ === SelectionMode.NONE) { + throw new Error('Selection is off.'); + } + + let rowInfo; + if (userRow === undefined) { + rowInfo = undefined; + } else { + rowInfo = this.tableRowsInfo_.get(userRow); + if (!rowInfo) { + throw new Error('Row has not been seen, must expand its parents.'); + } + } + + const e = this.prepareToChangeSelection_(); + + if (!rowInfo) { + this.selectedColumnIndex_ = undefined; + } else { + switch (this.selectionMode_) { + case SelectionMode.ROW: + this.selectedColumnIndex_ = undefined; + break; + + case SelectionMode.CELL: + if (this.selectedColumnIndex_ === undefined) { + const i = this.getFirstSelectableColumnIndex_(); + if (i === -1) { + throw new Error('Cannot find a selectable column.'); + } + this.selectedColumnIndex_ = i; + } + break; + + default: + throw new Error('Invalid selection mode ' + this.selectionMode_); + } + } + + this.selectedTableRowInfo_ = rowInfo; + this.updateSelectedState_(); + this.dispatchEvent(e); + }, + + prepareToChangeSelection_() { + const e = new tr.b.Event('selection-changed'); + const previousSelectedRowInfo = this.selectedTableRowInfo_; + if (previousSelectedRowInfo) { + e.previousSelectedTableRow = previousSelectedRowInfo.userRow; + } else { + e.previousSelectedTableRow = undefined; + } + + this.removeSelectedState_(); + + return e; + }, + + removeSelectedState_() { + this.setSelectedState_(false); + }, + + updateSelectedState_() { + this.setSelectedState_(true); + }, + + setSelectedState_(select) { + if (this.selectedTableRowInfo_ === undefined) return; + + // Row selection. + const rowNode = this.selectedTableRowInfo_.htmlNode; + if (select) { + Polymer.dom(rowNode).setAttribute('selected', true); + } else { + Polymer.dom(rowNode).removeAttribute('selected'); + } + + // Cell selection (if applicable). + const cellNode = Polymer.dom(rowNode).children[this.selectedColumnIndex_]; + if (!cellNode) return; + if (select) { + Polymer.dom(cellNode).setAttribute('selected', true); + } else { + Polymer.dom(cellNode).removeAttribute('selected'); + } + }, + + doesColumnIndexSupportSelection(columnIndex) { + const columnInfo = this.tableColumns_[columnIndex]; + const scs = columnInfo.supportsCellSelection; + if (scs === false) return false; + return true; + }, + + getFirstSelectableColumnIndex_() { + for (let i = 0; i < this.tableColumns_.length; i++) { + if (this.doesColumnIndexSupportSelection(i)) { + return i; + } + } + return -1; + }, + + getSelectableNodeGivenTableRowNode_(htmlNode) { + switch (this.selectionMode_) { + case SelectionMode.ROW: + return htmlNode; + + case SelectionMode.CELL: + return Polymer.dom(htmlNode).children[this.selectedColumnIndex_]; + + default: + throw new Error('Invalid selection mode ' + this.selectionMode_); + } + }, + + get selectedColumnIndex() { + if (this.selectionMode_ !== SelectionMode.CELL) { + return undefined; + } + return this.selectedColumnIndex_; + }, + + set selectedColumnIndex(selectedColumnIndex) { + this.rebuildIfNeeded_(); + if (this.selectionMode_ === SelectionMode.NONE) { + throw new Error('Selection is off.'); + } + if (selectedColumnIndex < 0 || + selectedColumnIndex >= this.tableColumns_.length) { + throw new Error('Invalid index'); + } + if (!this.doesColumnIndexSupportSelection(selectedColumnIndex)) { + throw new Error('Selection is not supported on this column'); + } + + const e = this.prepareToChangeSelection_(); + if (this.selectedColumnIndex_ === undefined) { + this.selectedTableRowInfo_ = undefined; + } else if (!this.selectedTableRowInfo_) { + if (this.tableRows_.length === 0) { + throw new Error('No available row to be selected'); + } + this.selectedTableRowInfo_ = + this.tableRowsInfo_.get(this.tableRows_[0]); + } + this.selectedColumnIndex_ = selectedColumnIndex; + this.updateSelectedState_(); + this.dispatchEvent(e); + }, + + onKeyDown_(e) { + if (this.selectionMode_ === SelectionMode.NONE) return; + + const CODE_TO_COMMAND_NAMES = { + 13: 'ENTER', + 32: 'SPACE', + 37: 'ARROW_LEFT', + 38: 'ARROW_UP', + 39: 'ARROW_RIGHT', + 40: 'ARROW_DOWN' + }; + const cmdName = CODE_TO_COMMAND_NAMES[e.keyCode]; + if (cmdName === undefined) return; + + e.stopPropagation(); + e.preventDefault(); + this.performKeyCommand_(cmdName); + }, + + onFocus_(e) { + // This method should be idempotent. If it can't be, then focus() must be + // updated. + if (this.selectionMode_ === SelectionMode.NONE || + this.selectedTableRow || + this.tableRows_.length === 0) { + return; + } + + if (this.selectionMode_ === SelectionMode.CELL && + this.getFirstSelectableColumnIndex_() === -1) { + // If there are no selectable columns in cell selection mode, don't do + // anything. + return; + } + + this.selectedTableRow = this.tableRows_[0]; + }, + + focus() { + this.$.body.focus(); + + // Need to manually call onFocus_ here: if the table is invisible for any + // reason, then the focus event will not fire, but the table may become + // visible later, and should reflect the focus accurately. + // If the table is already visible, then this will cause onFocus_ to be + // called multiple times. That shouldn't be a problem since onFocus_ is + // idempotent. + this.onFocus_(); + }, + + blur() { + this.$.body.blur(); + }, + + get isFocused() { + return this.root.activeElement === this.$.body; + }, + + performKeyCommand_(cmdName) { + this.rebuildIfNeeded_(); + + switch (cmdName) { + case 'ARROW_UP': + this.selectPreviousOrFirstRowIfPossible_(); + return; + + case 'ARROW_DOWN': + this.selectNextOrFirstRowIfPossible_(); + return; + + case 'ARROW_RIGHT': + switch (this.selectionMode_) { + case SelectionMode.NONE: + return; // No action. + case SelectionMode.ROW: + this.expandRowAndSelectChildRowIfPossible_(); + return; + case SelectionMode.CELL: + this.selectNextSelectableCellToTheRightIfPossible_(); + return; + default: + throw new Error('Invalid selection mode ' + this.selectionMode_); + } + + case 'ARROW_LEFT': + switch (this.selectionMode_) { + case SelectionMode.NONE: + return; // No action. + case SelectionMode.ROW: + this.collapseRowOrSelectParentRowIfPossible_(); + return; + case SelectionMode.CELL: + this.selectNextSelectableCellToTheLeftIfPossible_(); + return; + default: + throw new Error('Invalid selection mode ' + this.selectionMode_); + } + + case 'SPACE': + this.toggleRowExpansionStateIfPossible_(); + return; + + case 'ENTER': + this.stepIntoSelectionIfPossible_(); + return; + + default: + throw new Error('Unrecognized command ' + cmdName); + } + }, + + selectPreviousOrFirstRowIfPossible_() { + const prev = this.selectedTableRowInfo_ ? + this.selectedTableRowInfo_.htmlNode.previousElementSibling : + this.$.body.firstChild; + if (!prev) return; + + if (this.selectionMode_ === SelectionMode.CELL && + this.getFirstSelectableColumnIndex_() === -1) { + // If there are no selectable columns in cell selection mode, don't do + // anything. + return; + } + tr.ui.b.scrollIntoViewIfNeeded(prev); + this.selectedTableRow = prev.rowInfo.userRow; + }, + + selectNextOrFirstRowIfPossible_() { + this.getFirstSelectableColumnIndex_; + const next = this.selectedTableRowInfo_ ? + this.selectedTableRowInfo_.htmlNode.nextElementSibling : + this.$.body.firstChild; + if (!next) return; + + if (this.selectionMode_ === SelectionMode.CELL && + this.getFirstSelectableColumnIndex_() === -1) { + // If there are no selectable columns in cell selection mode, don't do + // anything. + return; + } + tr.ui.b.scrollIntoViewIfNeeded(next); + this.selectedTableRow = next.rowInfo.userRow; + }, + + expandRowAndSelectChildRowIfPossible_() { + const selectedRowInfo = this.selectedTableRowInfo_; + if (!selectedRowInfo || + selectedRowInfo.userRow[this.subRowsPropertyName_] === undefined || + selectedRowInfo.userRow[this.subRowsPropertyName_].length === 0) { + return; + } + if (!selectedRowInfo.isExpanded) { + this.setExpandedForTableRow(selectedRowInfo.userRow, true); + } + this.selectedTableRow = + selectedRowInfo.htmlNode.nextElementSibling.rowInfo.userRow; + }, + + collapseRowOrSelectParentRowIfPossible_() { + const selectedRowInfo = this.selectedTableRowInfo_; + if (!selectedRowInfo) return; + + if (selectedRowInfo.isExpanded) { + // If the node is expanded, collapse it. + this.setExpandedForTableRow(selectedRowInfo.userRow, false); + } else { + // If the node is not expanded, select its parent. + const parentRowInfo = selectedRowInfo.parentRowInfo; + if (parentRowInfo) { + this.selectedTableRow = parentRowInfo.userRow; + } + } + }, + + selectNextSelectableCellToTheRightIfPossible_() { + if (!this.selectedTableRowInfo_ || + this.selectedColumnIndex_ === undefined) { + return; + } + for (let i = this.selectedColumnIndex_ + 1; i < this.tableColumns_.length; + i++) { + if (this.doesColumnIndexSupportSelection(i)) { + this.selectedColumnIndex = i; + return; + } + } + }, + + selectNextSelectableCellToTheLeftIfPossible_() { + if (!this.selectedTableRowInfo_ || + this.selectedColumnIndex_ === undefined) { + return; + } + for (let i = this.selectedColumnIndex_ - 1; i >= 0; i--) { + if (this.doesColumnIndexSupportSelection(i)) { + this.selectedColumnIndex = i; + return; + } + } + }, + + toggleRowExpansionStateIfPossible_() { + const selectedRowInfo = this.selectedTableRowInfo_; + if (!selectedRowInfo || + selectedRowInfo.userRow[this.subRowsPropertyName_] === undefined || + selectedRowInfo.userRow[this.subRowsPropertyName_].length === 0) { + return; + } + this.setExpandedForTableRow(selectedRowInfo.userRow, + !selectedRowInfo.isExpanded); + }, + + stepIntoSelectionIfPossible_() { + if (!this.selectedTableRowInfo_) return; + this.dispatchStepIntoEvent_(this.selectedTableRowInfo_, + this.selectedColumnIndex_); + }, + + dispatchSortingChangedEvent_() { + const e = new tr.b.Event('sort-column-changed'); + e.sortColumnIndex = this.sortColumnIndex_; + e.sortDescending = this.sortDescending_; + this.dispatchEvent(e); + } + }); +})(); +</script> + +<dom-module id="tr-ui-b-table-header-cell"> + <template> + <style> + :host { + -webkit-user-select: none; + display: flex; + } + + span { + flex: 0 1 auto; + } + + #side { + -webkit-user-select: none; + flex: 0 0 auto; + padding-left: 2px; + padding-right: 2px; + vertical-align: top; + font-size: 15px; + font-family: sans-serif; + line-height: 85%; + margin-left: 5px; + } + + #side.disabled { + color: rgb(140, 140, 140); + } + + #title:empty, #side:empty { + display: none; + } + </style> + + <span id="title"></span> + <span id="side"></span> + </template> +</dom-module> +<script> +'use strict'; + +const ColumnAlignment = tr.ui.b.TableFormat.ColumnAlignment; + +Polymer({ + is: 'tr-ui-b-table-header-cell', + + created() { + this.tapCallback_ = undefined; + this.cellTitle_ = ''; + this.align_ = undefined; + this.selectable_ = false; + this.column_ = undefined; + }, + + ready() { + this.addEventListener('click', this.onTap_.bind(this)); + }, + + set column(column) { + this.column_ = column; + this.align = column.align; + this.cellTitle = column.title; + }, + + get column() { + return this.column_; + }, + + set cellTitle(value) { + this.cellTitle_ = value; + + const titleNode = tr.ui.b.asHTMLOrTextNode( + this.cellTitle_, this.ownerDocument); + + this.$.title.innerText = ''; + + Polymer.dom(this.$.title).appendChild(titleNode); + }, + + get cellTitle() { + return this.cellTitle_; + }, + + set align(align) { + switch (align) { + case undefined: + case ColumnAlignment.LEFT: + this.style.justifyContent = ''; + break; + + case ColumnAlignment.RIGHT: + this.style.justifyContent = 'flex-end'; + break; + + default: + throw new Error('Invalid alignment of column (title=\'' + + this.cellTitle_ + '\'): ' + align); + } + this.align_ = align; + }, + + get align() { + return this.align_; + }, + + clearSideContent() { + Polymer.dom(this.$.side).textContent = ''; + }, + + set sideContent(content) { + Polymer.dom(this.$.side).textContent = content; + this.$.side.style.display = content ? 'inline' : 'none'; + }, + + get sideContent() { + return Polymer.dom(this.$.side).textContent; + }, + + set sideContentDisabled(sideContentDisabled) { + this.$.side.classList.toggle('disabled', sideContentDisabled); + }, + + get sideContentDisabled() { + return this.$.side.classList.contains('disabled'); + }, + + set tapCallback(callback) { + this.style.cursor = 'pointer'; + this.tapCallback_ = callback; + }, + + get tapCallback() { + return this.tapCallback_; + }, + + onTap_() { + if (this.tapCallback_) { + this.tapCallback_(); + } + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/table_header_cell.html b/chromium/third_party/catapult/tracing/tracing/ui/base/table_header_cell.html new file mode 100644 index 00000000000..d7e8d427cb9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/table_header_cell.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> + +<dom-module id='tr-ui-b-table-header-cell'> + <template> + <style> + :host { + -webkit-user-select: none; + display: flex; + } + + span { + flex: 0 1 auto; + } + + side-element { + -webkit-user-select: none; + flex: 1 0 auto; + padding-left: 4px; + vertical-align: top; + font-size: 15px; + font-family: sans-serif; + display: inline; + line-height: 85%; + } + </style> + + <span id="title"></span><side-element id="side"></side-element> + </template> +</dom-module> + <script> + 'use strict'; + + Polymer({ + is: 'tr-ui-b-table-header-cell', + + listeners: { + 'tap': 'onTap_' + }, + + created() { + this.tapCallback_ = undefined; + this.cellTitle_ = ''; + }, + + set cellTitle(value) { + this.cellTitle_ = value; + + const titleNode = + tr.ui.b.asHTMLOrTextNode(this.cellTitle_, this.ownerDocument); + + this.$.title.innerText = ''; + Polymer.dom(this.$.title).appendChild(titleNode); + }, + + get cellTitle() { + return this.cellTitle_; + }, + + clearSideContent() { + Polymer.dom(this.$.side).textContent = ''; + }, + + set sideContent(content) { + Polymer.dom(this.$.side).textContent = content; + }, + + get sideContent() { + return Polymer.dom(this.$.side).textContent; + }, + + set tapCallback(callback) { + this.style.cursor = 'pointer'; + this.tapCallback_ = callback; + }, + + get tapCallback() { + return this.tapCallback_; + }, + + onTap_() { + if (this.tapCallback_) { + this.tapCallback_(); + } + } + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/table_test.html new file mode 100644 index 00000000000..73e8aca4418 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/table_test.html @@ -0,0 +1,2115 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const THIS_DOC = document.currentScript.ownerDocument; + const SelectionMode = tr.ui.b.TableFormat.SelectionMode; + const HighlightStyle = tr.ui.b.TableFormat.HighlightStyle; + const ColumnAlignment = tr.ui.b.TableFormat.ColumnAlignment; + + function isSelected(element) { + if (!element.hasAttribute('selected')) return false; + return element.getAttribute('selected') === 'true'; + } + + function simulateDoubleClick(element) { + // See https://developer.mozilla.org/en/docs/Web/API/MouseEvent#Example. + const event = new MouseEvent('dblclick', { + bubbles: true, + cancelable: true, + view: window + }); + return element.dispatchEvent(event); + } + + test('rowExpandedChanged', function() { + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = [ + { + title: 'Name', + value: row => row.value, + } + ]; + table.tableRows = [{value: 'a', subRows: [{value: 'b'}]}]; + let count = 0; + table.addEventListener('row-expanded-changed', e => ++count); + this.addHTMLOutput(table); + table.rebuild(); + + assert.strictEqual(0, count); + + table.setExpandedForTableRow(table.tableRows[0], true); + assert.strictEqual(1, count); + + table.setExpandedForTableRow(table.tableRows[0], true); + assert.strictEqual(1, count); + + table.setExpandedForTableRow(table.tableRows[0], false); + assert.strictEqual(2, count); + }); + + test('instantiateEmptyTable_withoutEmptyValue', function() { + // TODO(#4321): Switch to using skipped instead once it works + return; // https://github.com/catapult-project/catapult/issues/4320 + /* eslint-disable no-unreachable */ + const columns = [ + { + title: 'First Column', + value(row) { return row.firstData; }, + width: '300px' + }, + { + title: 'Second Column', + value(row) { return row.secondData; } + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = []; + table.rebuild(); + + this.addHTMLOutput(table); + + // Check that the width of the first column was set correctly (despite no + // body rows). + const firstColumnHeader = table.$.head.children[0].children[0]; + assert.closeTo(firstColumnHeader.offsetWidth, 300, 20); + + // Check that the first column has a non-empty header. + const firstColumnTitle = tr.ui.b.findDeepElementMatchingPredicate( + firstColumnHeader, function(element) { + return Polymer.dom(element).textContent === 'First Column'; + }); + assert.isDefined(firstColumnTitle); + + // Check that empty value was not appended. + assert.lengthOf(table.$.body.children, 0); + /* eslint-enable no-unreachable */ + }); + + test('instantiateEmptyTable_withEmptyValue', function() { + // TODO(#4321): Switch to using skipped instead once it works + return; // https://github.com/catapult-project/catapult/issues/4320 + /* eslint-disable no-unreachable */ + const columns = [ + { + title: 'First Column', + value(row) { return row.firstData; }, + width: '300px' + }, + { + title: 'Second Column', + value(row) { return row.secondData; } + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = []; + table.emptyValue = 'This table is left intentionally empty'; + table.rebuild(); + + this.addHTMLOutput(table); + + // Check that the width of the first column was set correctly (despite no + // body rows). + const firstColumnHeader = table.$.head.children[0].children[0]; + assert.closeTo(firstColumnHeader.offsetWidth, 300, 20); + + // Check that empty value was appended. + assert.lengthOf(table.$.body.children, 1); + /* eslint-enable no-unreachable */ + }); + + test('instantiateNestedTableNoNests', function() { + const columns = [ + { + title: 'First Column', + value(row) { return row.firstData; }, + width: '200px' + }, + { + title: 'Second Column', + value(row) { return row.secondData; } + } + ]; + + const rows = [ + { + firstData: 'A1', + secondData: 'A2' + }, + { + firstData: 'B1', + secondData: 'B2' + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = rows; + table.emptyValue = 'THIS SHOULD NOT BE VISIBLE!!!'; + table.rebuild(); + + this.addHTMLOutput(table); + + // Check that empty value was not appended. + assert.lengthOf(table.$.body.children, 2); + }); + + test('sequentialRebuildsBehaveSanely', function() { + const columns = [ + { + title: 'First Column', + value(row) { return row.firstData; }, + width: '200px' + }, + { + title: 'Second Column', + value(row) { return row.secondData; } + } + ]; + + const rows = [ + { + firstData: 'A1', + secondData: 'A2' + }, + { + firstData: 'B1', + secondData: 'B2' + } + ]; + const footerRows = [ + { + firstData: 'A1', + secondData: 'A2' + }, + { + firstData: 'B1', + secondData: 'B2' + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = rows; + table.footerRows = footerRows; + table.rebuild(); + table.rebuild(); + assert.strictEqual(table.$.body.children.length, 2); + assert.strictEqual(table.$.foot.children.length, 2); + + this.addHTMLOutput(table); + }); + + test('instantiateNestedTableWithNests', function() { + const columns = [ + { + title: 'First Column', + value(row) { return row.firstData; }, + width: '250px' + }, + { + title: 'Second Column', + value(row) { return row.secondData; }, + width: '50%' + } + ]; + + const rows = [ + { + firstData: 'A1', + secondData: 'A2', + subRows: [ + { + firstData: 'Sub1 A1', + secondData: 'Sub1 A2' + }, + { + firstData: 'Sub2 A1', + secondData: 'Sub2 A2', + subRows: [ + { + firstData: 'SubSub1 A1', + secondData: 'SubSub1 A2' + }, + { + firstData: 'SubSub2 A1', + secondData: 'SubSub2 A2' + } + ] + }, + { + firstData: 'Sub3 A1', + secondData: 'Sub3 A2' + } + ] + }, + { + firstData: 'B1', + secondData: 'B2' + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + + this.addHTMLOutput(table); + }); + + test('instantiateSortingCallbacksWithNests', function() { + const table = document.createElement('tr-ui-b-table'); + + const columns = [ + { + title: 'First Column', + value(row) { return row.firstData; }, + width: '50%' + }, + { + title: 'Second Column', + value(row) { return row.secondData; }, + width: '250px', + cmp(rowA, rowB) { + return rowA.secondData.toString().localeCompare( + rowB.secondData.toString()); + }, + showExpandButtons: true + } + ]; + + const rows = [ + { + firstData: 'A1', + secondData: 'A2', + subRows: [ + { + firstData: 'Sub1 A1', + secondData: 'Sub1 A2' + }, + { + firstData: 'Sub2 A1', + secondData: 'Sub2 A2', + subRows: [ + { + firstData: 'SubSub1 A1', + secondData: 'SubSub1 A2' + }, + { + firstData: 'SubSub2 A1', + secondData: 'SubSub2 A2' + } + ] + }, + { + firstData: 'Sub3 A1', + secondData: 'Sub3 A2' + } + ] + }, + { + firstData: 'B1', + secondData: 'B2' + } + ]; + + const footerRows = [ + { + firstData: 'F1', + secondData: 'F2', + subRows: [ + { + firstData: 'Sub1F1', + secondData: 'Sub1F2' + }, + { + firstData: 'Sub2F1', + secondData: 'Sub2F2', + subRows: [ + { + firstData: 'SubSub1F1', + secondData: 'SubSub1F2' + }, + { + firstData: 'SubSub2F1', + secondData: 'SubSub2F2' + } + ] + }, + { + firstData: 'Sub3F1', + secondData: 'Sub3F2' + } + ] + }, + { + firstData: 'F\'1', + secondData: 'F\'2' + } + + ]; + + table.tableColumns = columns; + table.tableRows = rows; + table.footerRows = footerRows; + table.rebuild(); + + this.addHTMLOutput(table); + + const button = THIS_DOC.createElement('button'); + Polymer.dom(button).textContent = 'Sort By Col 0'; + button.addEventListener('click', function() { + table.sortDescending = !table.sortDescending; + table.sortColumnIndex = 0; + }); + table.rebuild(); + + this.addHTMLOutput(button); + }); + + + test('instantiateNestedTableAlreadyExpanded', function() { + const columns = [ + { + title: 'a', + value(row) { return row.a; }, + width: '150px' + }, + { + title: 'a', + value(row) { return row.b; }, + width: '50%' + } + ]; + + const rows = [ + { + a: 'aToplevel', + b: 'bToplevel', + isExpanded: true, + subRows: [ + { + a: 'a1', + b: 'b1' + } + ] + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + this.addHTMLOutput(table); + + const a1El = tr.ui.b.findDeepElementMatchingPredicate( + table, e => Polymer.dom(e).textContent === 'a1'); + assert.isDefined(a1El); + + const bToplevelEl = tr.ui.b.findDeepElementMatchingPredicate( + table, + function(element) { + return Polymer.dom(element).textContent === 'bToplevel'; + }); + assert.isDefined(bToplevelEl); + const expandButton = Polymer.dom(bToplevelEl.parentElement) + .querySelector('expand-button'); + assert.isTrue(Polymer.dom(expandButton).classList.contains( + 'button-expanded')); + }); + + + test('subRowsThatAreRetrievedOnDemand', function() { + const columns = [ + { + title: 'a', + value(row) { return row.a; }, + width: '150px' + } + ]; + + const rows = [ + { + a: 'row1', + subRows: [ + { + b: 'row1.1', + get subRows() { + throw new Error('Shold not be called'); + } + } + ] + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + this.addHTMLOutput(table); + }); + + + test('instantiateTableWithHiddenHeader', function() { + const columns = [ + { + title: 'a', + value(row) { return row.a; }, + width: '150px' + }, + { + title: 'a', + value(row) { return row.b; }, + width: '50%' + } + ]; + + const rows = [ + { + a: 'aToplevel', + b: 'bToplevel' + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.showHeader = false; + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + this.addHTMLOutput(table); + + const tHead = table.$.head; + assert.strictEqual(table.$.head.children.length, 0); + assert.strictEqual(0, tHead.getBoundingClientRect().height); + + table.showHeader = true; + table.rebuild(); + table.showHeader = false; + table.rebuild(); + assert.strictEqual(table.$.head.children.length, 0); + }); + + + test('sortColumnsNotPossibleOnPercentSizedColumns', function() { + const columns = [ + { + title: 'Title', + value(row) { return row.a; }, + width: '150px' + }, + { + title: 'Value', + value(row) { return row.b; }, + width: '100%', + showExpandButtons: true + } + ]; + + const table1 = document.createElement('tr-ui-b-table'); + table1.showHeader = true; + + assert.throws(function() { + table1.tableColumns = columns; + }); + }); + + test('twoTablesFirstColumnMatching', function() { + const columns = [ + { + title: 'Title', + value(row) { return row.a; }, + width: '150px' + }, + { + title: 'Value', + value(row) { return row.b; }, + width: '100%' + } + ]; + + const table1 = document.createElement('tr-ui-b-table'); + table1.showHeader = true; + table1.tableColumns = columns; + table1.tableRows = [ + { + a: 'first', + b: 'row' + } + ]; + table1.rebuild(); + this.addHTMLOutput(table1); + + const table2 = document.createElement('tr-ui-b-table'); + table2.showHeader = false; + table2.tableColumns = columns; + table2.tableRows = [ + { + a: 'second', + b: 'row' + } + ]; + table2.rebuild(); + this.addHTMLOutput(table2); + + const h1FirstCol = table1.$.head.children[0].children[0]; + const h2FirstCol = table2.$.body.children[0].children[0]; + assert.strictEqual(h1FirstCol.getBoundingClientRect().width, + h2FirstCol.getBoundingClientRect().width); + }); + + test('programmaticSorting', function() { + const table = document.createElement('tr-ui-b-table'); + + const columns = [ + { + title: 'Column', + value(row) { return row.value; }, + cmp(rowA, rowB) { + return rowA.value.toString().localeCompare( + rowB.value.toString()); + } + } + ]; + + const rows = [ + { + value: 'A1', + subRows: [ + { + value: 'A1.1' + }, + { + value: 'A1.2', + subRows: [ + { + value: 'A1.2.1' + }, + { + value: 'A1.2.2' + } + ] + }, + { + value: 'A1.3' + } + ] + }, + { + value: 'A2' + } + ]; + + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + + this.addHTMLOutput(table); + + table.sortDescending = true; + table.sortColumnIndex = 0; + table.rebuild(); + const r0 = table.$.body.children[0]; + assert.strictEqual(r0.rowInfo.userRow, rows[1]); + + const r1 = table.$.body.children[1]; + assert.strictEqual(r1.rowInfo.userRow, rows[0]); + }); + + test('sortDispatchesEvent', function() { + const table = document.createElement('tr-ui-b-table'); + const columns = [ + { + title: 'Column 0', + value(row) { return row.value0; }, + cmp(rowA, rowB) { return rowA.value0 - rowB.value0; } + }, + { + title: 'Column 1', + value(row) { return row.value1; }, + cmp(rowA, rowB) { return rowA.value1 - rowB.value1; } + } + ]; + + let sortColumnIndex = undefined; + let sortDescending = undefined; + let numListenerCalls = 0; + table.tableColumns = columns; + table.addEventListener('sort-column-changed', function(e) { + sortColumnIndex = e.sortColumnIndex; + sortDescending = e.sortDescending; + numListenerCalls++; + }); + table.rebuild(); + + table.sortColumnIndex = 0; + assert.strictEqual(sortColumnIndex, 0); + assert.strictEqual(numListenerCalls, 1); + + table.sortDescending = true; + assert.strictEqual(sortColumnIndex, 0); + assert.isTrue(sortDescending); + assert.strictEqual(numListenerCalls, 2); + + table.sortColumnIndex = 1; + table.sortDescending = false; + assert.strictEqual(sortColumnIndex, 1); + assert.isFalse(sortDescending); + assert.strictEqual(numListenerCalls, 4); + + table.sortColumnIndex = undefined; + assert.strictEqual(sortColumnIndex, undefined); + assert.strictEqual(numListenerCalls, 5); + }); + + test('sortingAfterExpand', function() { + const table = document.createElement('tr-ui-b-table'); + + const columns = [ + { + title: 'Column', + value(row) { return row.value; }, + cmp(rowA, rowB) { + return rowA.value.toString().localeCompare( + rowB.value.toString()); + } + } + ]; + + const rows = [ + { + value: 'A1', + isExpanded: true, + subRows: [ + { + value: 'A1.1' + }, + { + value: 'A1.2', + subRows: [ + { + value: 'A1.2.1' + }, + { + value: 'A1.2.2' + } + ] + }, + { + value: 'A1.3' + } + ] + }, + { + value: 'A2' + } + ]; + + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + + this.addHTMLOutput(table); + + table.sortDescending = true; + table.sortColumnIndex = 0; + table.rebuild(); + const r0 = table.$.body.children[0]; + assert.strictEqual(r0.rowInfo.userRow, rows[1]); + + const r1 = table.$.body.children[1]; + assert.strictEqual(r1.rowInfo.userRow, rows[0]); + + const r2 = table.$.body.children[2]; + assert.strictEqual(r2.rowInfo.userRow, rows[0].subRows[2]); + + assert.isFalse(table.$.body.hasAttribute('tabindex')); + }); + + function createSimpleOneColumnNestedTable() { + const table = document.createElement('tr-ui-b-table'); + + const columns = [ + { + title: 'Column', + value(row) { return row.value; }, + cmp(rowA, rowB) { + return rowA.value.toString().localeCompare( + rowB.value.toString()); + } + } + ]; + + const rows = [ + { + value: 'A1', + subRows: [ + { + value: 'A1.1' + }, + { + value: 'A1.2', + subRows: [ + { + value: 'A1.2.1' + }, + { + value: 'A1.2.2' + } + ] + }, + { + value: 'A1.3' + } + ] + }, + { + value: 'A2' + } + ]; + + table.tableColumns = columns; + table.tableRows = rows; + return table; + } + + function createMultiColumnNestedTable() { + const table = document.createElement('tr-ui-b-table'); + + const columns = [ + { + title: 'Title', + value(row) { return row.value; }, + cmp(rowA, rowB) { + return rowA.value.toString().localeCompare( + rowB.value.toString()); + }, + width: '150px', + supportsCellSelection: false + }, + { + title: 'A', + value(row) { return row.a; }, + width: '25%' + }, + { + title: 'B', + value(row) { return row.b; }, + width: '25%' + }, + { + title: 'C', + value(row) { return row.c; }, + width: '25%', + supportsCellSelection: false + }, + { + title: 'D', + value(row) { return row.d; }, + width: '25%' + } + ]; + + const rows = [ + { + value: 'R1', + a: 1, b: 2, c: 3, d: 4, + subRows: [ + { + value: 'R1.1', + a: 2, b: 3, c: 4, d: 1, + }, + { + value: 'R1.2', + a: 3, b: 4, c: 1, d: 2, + } + ] + }, + { + value: 'R2', + a: 3, b: 4, c: 1, d: 2 + } + ]; + + table.tableColumns = columns; + table.tableRows = rows; + return table; + } + + test('expandAfterRebuild', function() { + const table = createSimpleOneColumnNestedTable(); + table.rebuild(); + const rows = table.tableRows; + + this.addHTMLOutput(table); + + table.rebuild(); + assert.isFalse(table.getExpandedForTableRow(rows[0])); + table.setExpandedForTableRow(rows[0], true); + assert.isTrue(table.getExpandedForTableRow(rows[0])); + + const r1 = table.$.body.children[1]; + assert.strictEqual(r1.rowInfo.userRow, rows[0].subRows[0]); + }); + + test('tableSelection', function() { + const table = createMultiColumnNestedTable(); + const rows = table.tableRows; + + table.selectionMode = SelectionMode.ROW; + table.selectedTableRow = rows[0]; + assert.isUndefined(table.selectedColumnIndex); + + table.setExpandedForTableRow(rows[0], true); + table.selectedTableRow = rows[0].subRows[1]; + assert.strictEqual(table.selectedTableRow, rows[0].subRows[1]); + assert.isUndefined(table.selectedColumnIndex); + + table.selectionMode = SelectionMode.CELL; + assert.strictEqual(table.selectedTableRow, rows[0].subRows[1]); + assert.strictEqual(table.selectedColumnIndex, 1); + + table.setExpandedForTableRow(rows[0], false); + assert.strictEqual(table.selectedTableRow, rows[0]); + assert.strictEqual(table.selectedColumnIndex, 1); + + table.selectionMode = SelectionMode.NONE; + assert.strictEqual(table.selectedTableRow, undefined); + + table.selectionMode = SelectionMode.ROW; + table.setExpandedForTableRow(rows[0].subRows[1], true); + this.addHTMLOutput(table); + + assert.isTrue(table.$.body.hasAttribute('tabindex')); + }); + + + test('keyMovement_rows', function() { + const table = createSimpleOneColumnNestedTable(); + table.selectionMode = SelectionMode.ROW; + this.addHTMLOutput(table); + + const rows = table.tableRows; + table.selectedTableRow = rows[0]; + + table.performKeyCommand_('ARROW_DOWN'); + assert.strictEqual(table.selectedTableRow, rows[1]); + + table.performKeyCommand_('ARROW_UP'); + assert.strictEqual(table.selectedTableRow, rows[0]); + + // Enter on collapsed row should expand. + table.selectedTableRow = rows[0]; + table.performKeyCommand_('SPACE'); + assert.strictEqual(table.selectedTableRow, rows[0]); + assert.isTrue(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('SPACE'); + assert.isFalse(table.getExpandedForTableRow(rows[0])); + + // Arrow right on collapsed row should expand. + table.selectedTableRow = rows[0]; + table.performKeyCommand_('ARROW_RIGHT'); + assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]); + assert.isTrue(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('ARROW_DOWN'); + assert.strictEqual(table.selectedTableRow, rows[0].subRows[1]); + + // Arrow left on collapsed item should select parent. + table.performKeyCommand_('ARROW_LEFT'); + assert.strictEqual(table.selectedTableRow, rows[0]); + assert.isTrue(table.getExpandedForTableRow(rows[0])); + // Arrow left on parent should collapse its children. + table.performKeyCommand_('ARROW_LEFT'); + assert.isFalse(table.getExpandedForTableRow(rows[0])); + + // Arrow right on expanded row should select first child. + table.selectedTableRow = rows[0]; + table.setExpandedForTableRow(rows[0], true); + table.performKeyCommand_('ARROW_RIGHT'); + assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]); + + // Arrow right on a non-expandable row should do nothing. + table.selectedTableRow = rows[1]; + assert.strictEqual(table.selectedTableRow, rows[1]); + table.performKeyCommand_('ARROW_RIGHT'); + assert.strictEqual(table.selectedTableRow, rows[1]); + assert.isFalse(table.getExpandedForTableRow(rows[1])); + }); + + test('keyMovement_cells', function() { + const table = createMultiColumnNestedTable(); + table.selectionMode = SelectionMode.CELL; + this.addHTMLOutput(table); + + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + const rows = table.tableRows; + table.selectedTableRow = rows[1]; + assert.strictEqual(table.selectedColumnIndex, 1); + + table.performKeyCommand_('ARROW_LEFT'); + assert.strictEqual(table.selectedTableRow, rows[1]); + // No-op (leftmost selectable cell already selected). + assert.strictEqual(table.selectedColumnIndex, 1); + + table.performKeyCommand_('ARROW_UP'); + // No-op (top row already selected). + assert.strictEqual(table.selectedTableRow, rows[0]); + assert.strictEqual(table.selectedColumnIndex, 1); + + table.performKeyCommand_('ARROW_UP'); + assert.strictEqual(table.selectedTableRow, rows[0]); + assert.strictEqual(table.selectedColumnIndex, 1); + + table.performKeyCommand_('ARROW_RIGHT'); + assert.strictEqual(table.selectedTableRow, rows[0]); + assert.strictEqual(table.selectedColumnIndex, 2); + // Right arrow should NOT expand nested rows in cell selection mode. + assert.isFalse(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('ARROW_RIGHT'); + assert.strictEqual(table.selectedTableRow, rows[0]); + assert.strictEqual(table.selectedColumnIndex, 4); + assert.isFalse(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('ARROW_RIGHT'); + assert.strictEqual(table.selectedTableRow, rows[0]); + // No-op (rightmost selectable cell already selected). + assert.strictEqual(table.selectedColumnIndex, 4); + assert.isFalse(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('SPACE'); + assert.strictEqual(table.selectedTableRow, rows[0]); + assert.strictEqual(table.selectedColumnIndex, 4); + // Space on collapsed row should expand it. + assert.isTrue(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('ARROW_DOWN'); + assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]); + assert.strictEqual(table.selectedColumnIndex, 4); + assert.isTrue(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('ARROW_LEFT'); + // Left arrow should NOT move to parent row. + assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]); + assert.strictEqual(table.selectedColumnIndex, 2); + assert.isTrue(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('ARROW_LEFT'); + assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]); + assert.strictEqual(table.selectedColumnIndex, 1); + assert.isTrue(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('ARROW_LEFT'); + assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]); + // No-op (leftmost selectable cell already selected). + assert.strictEqual(table.selectedColumnIndex, 1); + assert.isTrue(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('ARROW_UP'); + assert.strictEqual(table.selectedTableRow, rows[0]); + assert.strictEqual(table.selectedColumnIndex, 1); + assert.isTrue(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('ARROW_LEFT'); + assert.strictEqual(table.selectedTableRow, rows[0]); + // No-op (leftmost selectable cell already selected). + assert.strictEqual(table.selectedColumnIndex, 1); + // Left arrow should NOT collapse nested rows in cell selection mode. + assert.isTrue(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('SPACE'); + assert.strictEqual(table.selectedTableRow, rows[0]); + assert.strictEqual(table.selectedColumnIndex, 1); + // Space on expanded row should collapse it. + assert.isFalse(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('ARROW_DOWN'); + assert.strictEqual(table.selectedTableRow, rows[1]); + assert.strictEqual(table.selectedColumnIndex, 1); + assert.isFalse(table.getExpandedForTableRow(rows[0])); + + table.performKeyCommand_('ARROW_DOWN'); + // No-op (bottom row already selected). + assert.strictEqual(table.selectedTableRow, rows[1]); + assert.strictEqual(table.selectedColumnIndex, 1); + assert.isFalse(table.getExpandedForTableRow(rows[0])); + }); + + test('focus_empty', function() { + const table = createSimpleOneColumnNestedTable(); + table.tableRows = []; + table.emptyValue = 'This table is left intentionally empty'; + this.addHTMLOutput(table); + + assert.isFalse(table.$.body.hasAttribute('tabindex')); + assert.isFalse(table.isFocused); + + for (const selectionMode of [SelectionMode.ROW, SelectionMode.CELL]) { + table.selectionMode = selectionMode; + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Manually focus. + table.focus(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isTrue(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Manually unfocus. + table.blur(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Manually focus again. + table.focus(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isTrue(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Unfocus via removing selection mode. + table.selectionMode = SelectionMode.NONE; + assert.isFalse(table.$.body.hasAttribute('tabindex')); + assert.isFalse(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + } + + // Re-enable selection mode (for interactive testing). + table.selectionMode = SelectionMode.ROW; + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + }); + + test('focus_rows', function() { + const table = createSimpleOneColumnNestedTable(); + table.selectionMode = SelectionMode.ROW; + this.addHTMLOutput(table); + + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Manually focus. + table.focus(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isTrue(table.isFocused); + assert.strictEqual(table.selectedTableRow, table.tableRows[0]); + assert.isUndefined(table.selectedColumnIndex); + + // Manually unfocus. + table.blur(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.strictEqual(table.selectedTableRow, table.tableRows[0]); + assert.isUndefined(table.selectedColumnIndex); + + // Trigger focus via clicking. + table.$.body.children[1].children[0].click(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isTrue(table.isFocused); + assert.strictEqual(table.selectedTableRow, table.tableRows[1]); + assert.isUndefined(table.selectedColumnIndex); + + // Unfocus via removing selection mode. + table.selectionMode = SelectionMode.NONE; + assert.isFalse(table.$.body.hasAttribute('tabindex')); + assert.isFalse(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Re-enable selection mode. + table.selectionMode = SelectionMode.ROW; + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Programatically select row (should NOT steal focus). + table.selectedTableRow = table.tableRows[0]; + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.strictEqual(table.selectedTableRow, table.tableRows[0]); + assert.isUndefined(table.selectedColumnIndex); + + // Trigger focus on the already selected row by clicking. + table.$.body.children[0].children[0].click(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isTrue(table.isFocused); + assert.strictEqual(table.selectedTableRow, table.tableRows[0]); + assert.isUndefined(table.selectedColumnIndex); + }); + + test('focus_cells', function() { + const table = createMultiColumnNestedTable(); + table.selectionMode = SelectionMode.CELL; + this.addHTMLOutput(table); + + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Manually focus. + table.focus(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isTrue(table.isFocused); + assert.strictEqual(table.selectedTableRow, table.tableRows[0]); + assert.strictEqual(table.selectedColumnIndex, 1); + + // Manually unfocus. + table.blur(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.strictEqual(table.selectedTableRow, table.tableRows[0]); + assert.strictEqual(table.selectedColumnIndex, 1); + + // Trigger focus via clicking. + table.$.body.children[1].children[4].click(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isTrue(table.isFocused); + assert.strictEqual(table.selectedTableRow, table.tableRows[1]); + assert.strictEqual(table.selectedColumnIndex, 4); + + // Unfocus via removing selection mode. + table.selectionMode = SelectionMode.NONE; + assert.isFalse(table.$.body.hasAttribute('tabindex')); + assert.isFalse(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Re-enable selection mode. + table.selectionMode = SelectionMode.CELL; + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Clicking on an unselectable cell should NOT trigger focus. + table.$.body.children[1].children[0].click(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Programatically select cell (should NOT steal focus). + table.selectedTableRow = table.tableRows[0]; + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.strictEqual(table.selectedTableRow, table.tableRows[0]); + assert.strictEqual(table.selectedColumnIndex, 1); + + // Trigger focus on the already selected cell by clicking. + table.$.body.children[0].children[1].click(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isTrue(table.isFocused); + assert.strictEqual(table.selectedTableRow, table.tableRows[0]); + assert.strictEqual(table.selectedColumnIndex, 1); + }); + + test('focus_allCellsUnselectable', function() { + const table = createMultiColumnNestedTable(); + table.selectionMode = SelectionMode.CELL; + for (const c of table.tableColumns) { + c.supportsCellSelection = false; + } + table.tableColumns = table.tableColumns; + this.addHTMLOutput(table); + + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isFalse(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Manually focus (no automatic selection). + table.focus(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isTrue(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + + // Trigger focus via clicking (no selection). + table.$.body.children[1].children[2].click(); + assert.strictEqual(table.$.body.getAttribute('tabindex'), '0'); + assert.isTrue(table.isFocused); + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + }); + + test('RightArrowKeyWhenTableSorted', function() { + const table = createSimpleOneColumnNestedTable(); + table.selectionMode = SelectionMode.ROW; + this.addHTMLOutput(table); + table.sortDescending = true; + table.sortColumnIndex = 0; + table.rebuild(); + const rows = table.tableRows; + + // Arrow right should select the first child showing up on the viewer, + // rather than first child in sub rows since sorted. + table.selectedTableRow = rows[0]; + table.performKeyCommand_('ARROW_RIGHT'); + assert.strictEqual(table.selectedTableRow, rows[0].subRows[2]); + }); + + test('reduceNumberOfColumnsAfterRebuild', function() { + // Create a table with two columns. + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = [ + { + title: 'First Column', + value(row) { return row.firstData; }, + width: '100px' + }, + { + title: 'Second Column', + value(row) { return row.secondData; }, + width: '100px' + } + ]; + + // Build the table. + table.rebuild(); + + // Check that reducing the number of columns doesn't throw an exception. + table.tableColumns = [ + { + title: 'First Column', + value(row) { return row.firstData; }, + width: '200px' + } + ]; + }); + + test('rowHighlightDark', function() { + const columns = [ + { + title: 'Title', + value(row) { return row.a; }, + width: '150px', + supportsCellSelection: false + }, + { + title: 'Col1', + value(row) { return row.b; }, + width: '33%' + }, + { + title: 'Col2', + value(row) { return row.b * 2; }, + width: '33%' + }, + { + title: 'Col3', + value(row) { return row.b * 3; }, + width: '33%' + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.showHeader = true; + table.rowHighlightStyle = HighlightStyle.DARK; + table.tableColumns = columns; + table.tableRows = [ + { + a: 'first', + b: '1' + }, + { + a: 'second', + b: '2' + } + ]; + table.rebuild(); + this.addHTMLOutput(table); + }); + + test('cellHighlightLight', function() { + const columns = [ + { + title: 'Title', + value(row) { return row.a; }, + width: '150px', + supportsCellSelection: false + }, + { + title: 'Col1', + value(row) { return row.b; }, + width: '33%' + }, + { + title: 'Col2', + value(row) { return row.b * 2; }, + width: '33%' + }, + { + title: 'Col3', + value(row) { return row.b * 3; }, + width: '33%' + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.showHeader = true; + table.cellHighlightStyle = HighlightStyle.LIGHT; + table.tableColumns = columns; + table.tableRows = [ + { + a: 'first', + b: '1' + }, + { + a: 'second', + b: '2' + } + ]; + table.rebuild(); + this.addHTMLOutput(table); + }); + + test('cellSelectionBasic', function() { + const columns = [ + { + title: 'Title', + value(row) { return row.a; }, + width: '150px', + supportsCellSelection: false + }, + { + title: 'Col1', + value(row) { return row.b; }, + width: '33%' + }, + { + title: 'Col2', + value(row) { return row.b * 2; }, + width: '33%' + }, + { + title: 'Col3', + value(row) { return row.b * 3; }, + width: '33%' + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.showHeader = true; + table.selectionMode = SelectionMode.CELL; + table.rowHighlightStyle = HighlightStyle.NONE; + table.tableColumns = columns; + table.tableRows = [ + { + a: 'first', + b: '1' + }, + { + a: 'second', + b: '2' + } + ]; + table.rebuild(); + this.addHTMLOutput(table); + + table.selectedTableRow = table.tableRows[0]; + assert.strictEqual(table.selectedColumnIndex, 1); + let selectedCell = table.selectedCell; + assert.strictEqual(selectedCell.row, table.tableRows[0]); + assert.strictEqual(selectedCell.column, columns[1]); + assert.strictEqual(selectedCell.value, '1'); + + table.performKeyCommand_('ARROW_DOWN'); + table.performKeyCommand_('ARROW_RIGHT'); + table.performKeyCommand_('ARROW_RIGHT'); + table.performKeyCommand_('ARROW_LEFT'); + assert.strictEqual(table.selectedTableRow, table.tableRows[1]); + assert.strictEqual(table.selectedColumnIndex, 2); + selectedCell = table.selectedCell; + assert.strictEqual(selectedCell.row, table.tableRows[1]); + assert.strictEqual(selectedCell.column, columns[2]); + assert.strictEqual(selectedCell.value, 4); + + table.selectedTableRow = undefined; + assert.isUndefined(table.selectedTableRow); + assert.isUndefined(table.selectedColumnIndex); + assert.isUndefined(table.selectedColumnIndex); + assert.isUndefined(table.selectedCell); + }); + + test('cellSelectionNested', function() { + const columns = [ + { + title: 'Title', + value(row) { return row.a; }, + width: '150px', + supportsCellSelection: false + }, + { + title: 'Value', + value(row) { return row.b; }, + width: '150px' + } + ]; + + const rows = [ + { + a: 'parent', + b: '1', + subRows: [ + { + a: 'child', + b: '2' + } + ] + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.showHeader = true; + table.selectionMode = SelectionMode.CELL; + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + this.addHTMLOutput(table); + + // Expand the parent row. + table.setExpandedForTableRow(rows[0], true); + + // Select the second cell in the child row. + table.selectedTableRow = rows[0].subRows[0]; + assert.isFalse(isSelected(table.$.body.children[0])); + assert.isFalse(isSelected(table.$.body.children[0].children[1])); + assert.isTrue(isSelected(table.$.body.children[1])); + assert.isTrue(isSelected(table.$.body.children[1].children[1])); + + // Fold the parent row. The second cell in the parent row should be + // automatically selected. + table.setExpandedForTableRow(rows[0], false); + assert.isTrue(isSelected(table.$.body.children[0])); + assert.isTrue(isSelected(table.$.body.children[0].children[1])); + + // Expand the parent row again. Only the second cell of the parent row + // should still be selected. + table.setExpandedForTableRow(rows[0], true); + assert.isTrue(isSelected(table.$.body.children[0])); + assert.isTrue(isSelected(table.$.body.children[0].children[1])); + assert.isFalse(isSelected(table.$.body.children[1])); + assert.isFalse(isSelected(table.$.body.children[1].children[1])); + }); + + test('resolvedHighlightStyle', function() { + const table = document.createElement('tr-ui-b-table'); + + // Undefined selection mode. + assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.NONE); + assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.NONE); + + // Row selection mode. + table.selectionMode = SelectionMode.ROW; + assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.DARK); + assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.NONE); + + // Cell selection mode. + table.selectionMode = SelectionMode.CELL; + assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.LIGHT); + assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.DARK); + + // Explicit row highlight style. + table.rowHighlightStyle = HighlightStyle.NONE; + assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.NONE); + assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.DARK); + + // Explicit row and cell highlight styles. + table.cellHighlightStyle = HighlightStyle.LIGHT; + assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.NONE); + assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.LIGHT); + + // Back to default highlight styles. + table.cellHighlightStyle = HighlightStyle.DEFAULT; + table.rowHighlightStyle = HighlightStyle.DEFAULT; + assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.LIGHT); + assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.DARK); + }); + + test('headersWithHtmlElements', function() { + const firstColumnTitle = document.createTextNode('First Column'); + const secondColumnTitle = document.createElement('span'); + secondColumnTitle.innerText = 'Second Column'; + secondColumnTitle.style.color = 'blue'; + + const columns = [ + { + title: firstColumnTitle, + value(row) { return row.firstData; }, + width: '200px' + }, + { + title: secondColumnTitle, + value(row) { return row.secondData; } + } + ]; + + const rows = [ + { + firstData: 'A1', + secondData: 'A2' + }, + { + firstData: 'B1', + secondData: 'B2' + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + + this.addHTMLOutput(table); + + const firstColumnHeader = table.$.head.children[0].children[0].children[0]; + const secondColumnHeader = table.$.head.children[0].children[1].children[0]; + assert.strictEqual(Polymer.dom(firstColumnHeader.cellTitle).textContent, + 'First Column'); + assert.strictEqual(Polymer.dom(secondColumnHeader.cellTitle).textContent, + 'Second Column'); + }); + + test('align', function() { + const columns = [ + { + title: 'a', + align: ColumnAlignment.RIGHT, + value(row) { + return row.a; + } + } + ]; + const rows = [{a: 1}, {a: 'long-row-so-that-alignment-would-be-visible'}]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + + this.addHTMLOutput(table); + + assert.strictEqual( + table.$.body.children[0].children[0].style.textAlign, 'right'); + }); + + test('subRowsPropertyName', function() { + const columns = [ + { + title: 'a', + value(row) { + return row.a; + } + } + ]; + const rows = [ + { + a: 1, + isExpanded: true, + children: [ + {a: 2} + ] + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.subRowsPropertyName = 'children'; + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + + this.addHTMLOutput(table); + + assert.strictEqual( + '2', Polymer.dom(table.$.body.children[1].children[0]).textContent); + }); + + test('shouldNotRenderUndefined', function() { + const columns = [ + { + title: 'Column', + value(row) { return row.firstData; } + } + ]; + + const rows = [ + { + firstData: undefined, + secondData: 'A2' + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + + this.addHTMLOutput(table); + + // check that we don't have 'undefined' anywhere + assert.isTrue(Polymer.dom(table.$.body).innerHTML.indexOf('undefined') < 0); + }); + + test('customizeTableRowCallback', function() { + const columns = [ + { + title: 'Column', + value(row) { return row.data; } + } + ]; + + const rows = [ + { + data: 'data' + } + ]; + + const table = document.createElement('tr-ui-b-table'); + let callbackCalled = false; + table.tableColumns = columns; + table.tableRows = rows; + table.customizeTableRowCallback = function(userRow, trElement) { + callbackCalled = (userRow === rows[0]); + }; + table.rebuild(); + assert.isTrue(callbackCalled); + + this.addHTMLOutput(table); + + // The callback can also be set after the table is first built. + table.customizeTableRowCallback = function(userRow, trElement) { + callbackCalled = (userRow === rows[0]); + }; + + // Setting the customize callback should set the body dirty. + assert.isTrue(table.bodyDirty_); + + callbackCalled = false; + + // Don't bother waiting for the timeout. + table.rebuild(); + + assert.isTrue(callbackCalled); + }); + + test('selectionEdgeCases', function() { + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = [ + { + title: 'Column', + value(row) { return row.data; }, + supportsCellSelection: false + } + ]; + table.tableRows = [{ data: 'body row' }]; + table.footerRows = [{ data: 'footer row' }]; + table.selectionMode = SelectionMode.ROW; + this.addHTMLOutput(table); + + // Clicking on the body row should *not* throw an exception (despite the + // column not supporting cell selection). + table.$.body.children[0].children[0].click(); + + // Clicking on the footer row should *not* throw an exception (despite + // footer rows not being selectable in general). + table.$.foot.children[0].children[0].click(); + }); + + test('defaultExpansionStateCallback', function() { + const columns = [ + { + title: 'Name', + value(row) { return row.name; } + }, + { + title: 'Value', + value(row) { return row.value; } + } + ]; + + const rows = [ + { + name: 'A', + value: 10, + subRows: [ + { + name: 'B', + value: 8, + subRows: [ + { + name: 'C', + value: 4 + }, + { + name: 'D', + value: 4 + } + ] + }, + { + name: 'E', + value: 2, + subRows: [ + { + name: 'F', + value: 1 + }, + { + name: 'G', + value: 1 + } + ] + } + ] + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + + this.addHTMLOutput(table); + + let cRow = tr.ui.b.findDeepElementMatchingPredicate( + table, function(element) { + return Polymer.dom(element).textContent === 'C'; + }); + assert.strictEqual(cRow, undefined); + + let callbackCalled = false; + table.defaultExpansionStateCallback = function(row, parentRow) { + callbackCalled = true; + + if (parentRow === undefined) return true; + + if (row.value >= (parentRow.value * 0.8)) return true; + + return false; + }; + + // Setting the callback should set the body dirty. + assert.isTrue(table.bodyDirty_); + assert.isFalse(callbackCalled); + + table.rebuild(); + + assert.isTrue(callbackCalled); + cRow = tr.ui.b.findDeepElementMatchingPredicate(table, function(element) { + return Polymer.dom(element).textContent === 'C'; + }); + assert.isDefined(cRow); + }); + + test('sortExpanded', function() { + const columns = [ + { + title: 'Name', + value(row) { return row.name; } + }, + { + title: 'Value', + value(row) { return row.value; }, + cmp(x, y) { return x.value - y.value; } + } + ]; + + const rows = [ + { + name: 'A', + value: 10, + subRows: [ + { + name: 'B', + value: 8 + }, + { + name: 'C', + value: 4 + }, + ] + } + ]; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + + this.addHTMLOutput(table); + + function isB(row) { + return row.textContent === 'B'; + } + + // Check that 'A' row is not expanded. + assert.isUndefined(tr.ui.b.findDeepElementMatchingPredicate(table, isB)); + + // Expand 'A' row. + table.setExpandedForTableRow(rows[0], true); + + // Check that 'A' is expanded. + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(table, isB)); + + // Sort by value. + table.sortColumnIndex = 1; + + // Rebuild the table synchronously instead of waiting for scheduleRebuild_'s + // setTimeout(0). + table.rebuild(); + + // Check that 'A' is still expanded. + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(table, isB)); + }); + + test('shouldPreserveSortWhenColumnsChange', function() { + const name = { + title: 'Name', + value(row) { return row.name; }, + }; + + const count = { + title: 'Count', + value(row) { return row.count; }, + cmp: (rowA, rowB) => rowA.a - rowB.a, + }; + + const otherCount = { + title: 'Count', + value(row) { return row.count; }, + cmp: (rowA, rowB) => rowA.a - rowB.a, + }; + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = [count]; + table.sortColumnIndex = 0; + table.sortDescending = true; + table.rebuild(); + + this.addHTMLOutput(table); + + table.tableColumns = [name, count]; + table.rebuild(); + assert.strictEqual(1, table.sortColumnIndex); + assert.isTrue(table.sortDescending); + + table.sortDescending = false; + table.tableColumns = [otherCount, name]; + table.rebuild(); + assert.strictEqual(0, table.sortColumnIndex); + assert.isFalse(table.sortDescending); + + table.tableColumns = [name]; + table.rebuild(); + assert.isUndefined(table.sortColumnIndex); + }); + + test('userCanModifySortOrder', function() { + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = [ + { + title: 'Name', + value: row => row.name + }, + { + title: 'colA', + value: row => row.a, + cmp: (rowA, rowB) => rowA.a - rowB.a + }, + { + title: 'colB', + value: row => row.b, + cmp: (rowA, rowB) => rowA.b - rowB.b + } + ]; + table.tableRows = [ + {name: 'A', a: 42, b: 0}, + {name: 'B', a: 89, b: 100}, + {name: 'C', a: 65, b: -273.15} + ]; + table.userCanModifySortOrder = false; + table.sortColumnIndex = 2; + table.sortDescending = true; + table.rebuild(); + this.addHTMLOutput(table); + + const toggleButton = document.createElement('button'); + Polymer.dom(toggleButton).textContent = + 'Toggle table.userCanModifySortOrder'; + toggleButton.addEventListener('click', function() { + table.userCanModifySortOrder = !table.userCanModifySortOrder; + }); + this.addHTMLOutput(toggleButton); + + const unsetButton = document.createElement('button'); + Polymer.dom(unsetButton).textContent = 'Unset sort order'; + unsetButton.addEventListener('click', function() { + table.sortColumnIndex = undefined; + }); + this.addHTMLOutput(unsetButton); + }); + + test('columnSelection', function() { + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = [ + { + title: 'Name', + value: (row) => row.name + }, + { + title: 'colA', + selectable: true, + value: (row) => row.a, + cmp: (rowA, rowB) => rowA.a - rowB.a + }, + { + title: 'colB', + selectable: true, + value: (row) => row.b, + cmp: (rowA, rowB) => rowA.b - rowB.b + } + ]; + table.tableRows = [ + {name: 'foo', a: 42, b: -42}, + {name: 'bar', a: 57, b: 133} + ]; + table.rebuild(); + table.selectionMode = SelectionMode.CELL; + this.addHTMLOutput(table); + + table.selectedTableColumnIndex = 1; + let cols = tr.ui.b.findDeepElementMatchingPredicate(table, + e => e.tagName === 'COLGROUP').children; + assert.isNull(cols[0].getAttribute('selected')); + assert.strictEqual(cols[1].getAttribute('selected'), 'true'); + assert.isNull(cols[2].getAttribute('selected')); + assert.strictEqual(1, table.selectedTableColumnIndex); + + table.selectedTableColumnIndex = undefined; + cols = tr.ui.b.findDeepElementMatchingPredicate(table, + e => e.tagName === 'COLGROUP').children; + assert.isNull(cols[0].getAttribute('selected')); + assert.isNull(cols[1].getAttribute('selected')); + assert.isNull(cols[2].getAttribute('selected')); + assert.isUndefined(table.selectedTableColumnIndex); + + table.selectedTableColumnIndex = 2; + cols = tr.ui.b.findDeepElementMatchingPredicate(table, + e => e.tagName === 'COLGROUP').children; + assert.isNull(cols[0].getAttribute('selected')); + assert.isNull(cols[1].getAttribute('selected')); + assert.strictEqual(cols[2].getAttribute('selected'), 'true'); + assert.strictEqual(2, table.selectedTableColumnIndex); + }); + + test('stepInto', function() { + const columns = [ + { + title: 'Title', + value(row) { return row.a; }, + width: '150px', + supportsCellSelection: false + }, + { + title: 'Col1', + value(row) { return row.b; }, + width: '33%' + }, + { + title: 'Col2', + value(row) { return row.b * 2; }, + width: '33%' + }, + { + title: 'Col3', + value(row) { return row.b * 3; }, + width: '33%' + } + ]; + const rows = [ + { + a: 'first', + b: '1' + }, + { + a: 'second', + b: '2' + } + ]; + + const table = document.createElement('tr-ui-b-table'); + + const firedStepIntoEvents = []; + table.addEventListener('step-into', e => firedStepIntoEvents.push(e)); + + table.cellHighlightStyle = HighlightStyle.DARK; + table.tableColumns = columns; + table.tableRows = rows; + table.rebuild(); + this.addHTMLOutput(table); + + assert.lengthOf(firedStepIntoEvents, 0); + + // Double click. + simulateDoubleClick(table.$.body.children[0].children[1]); + assert.lengthOf(firedStepIntoEvents, 1); + assert.strictEqual(firedStepIntoEvents[0].tableRow, rows[0]); + assert.strictEqual(firedStepIntoEvents[0].tableColumn, columns[1]); + assert.strictEqual(firedStepIntoEvents[0].columnIndex, 1); + + simulateDoubleClick(table.$.body.children[1].children[3]); + assert.lengthOf(firedStepIntoEvents, 2); + assert.strictEqual(firedStepIntoEvents[1].tableRow, rows[1]); + assert.strictEqual(firedStepIntoEvents[1].tableColumn, columns[3]); + assert.strictEqual(firedStepIntoEvents[1].columnIndex, 3); + + // Shift+Enter in cell selection mode. + table.selectionMode = SelectionMode.CELL; + table.selectedTableRow = rows[0]; + table.selectedColumnIndex = 2; + table.performKeyCommand_('ENTER'); + assert.lengthOf(firedStepIntoEvents, 3); + assert.strictEqual(firedStepIntoEvents[2].tableRow, rows[0]); + assert.strictEqual(firedStepIntoEvents[2].tableColumn, columns[2]); + assert.strictEqual(firedStepIntoEvents[2].columnIndex, 2); + + // Shift+Enter in row selection mode. + table.selectionMode = SelectionMode.ROW; + table.selectedTableRow = rows[1]; + table.performKeyCommand_('ENTER'); + assert.lengthOf(firedStepIntoEvents, 4); + assert.strictEqual(firedStepIntoEvents[3].tableRow, rows[1]); + assert.isUndefined(firedStepIntoEvents[3].tableColumn); + assert.isUndefined(firedStepIntoEvents[3].columnIndex); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool.html b/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool.html new file mode 100644 index 00000000000..e142afd50f1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool.html @@ -0,0 +1,327 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/slice.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the TimingTool class. + */ +tr.exportTo('tr.ui.b', function() { + /** + * Tool for taking time measurements in the TimelineTrackView using + * Viewportmarkers. + * @constructor + */ + function TimingTool(viewport, targetElement) { + this.viewport_ = viewport; + + // Prepare the event handlers to be added and removed repeatedly. + this.onMouseMove_ = this.onMouseMove_.bind(this); + this.onDblClick_ = this.onDblClick_.bind(this); + this.targetElement_ = targetElement; + + // Valid only during mousedown. + this.isMovingLeftEdge_ = false; + } + + TimingTool.prototype = { + + onEnterTiming(e) { + this.targetElement_.addEventListener('mousemove', this.onMouseMove_); + this.targetElement_.addEventListener('dblclick', this.onDblClick_); + }, + + onBeginTiming(e) { + if (!this.isTouchPointInsideTrackBounds_(e.clientX, e.clientY)) { + return; + } + + const pt = this.getSnappedToEventPosition_(e); + this.mouseDownAt_(pt.x, pt.y); + + this.updateSnapIndicators_(pt); + }, + + updateSnapIndicators_(pt) { + if (!pt.snapped) return; + + const ir = this.viewport_.interestRange; + if (ir.min === pt.x) { + ir.leftSnapIndicator = new tr.ui.SnapIndicator(pt.y, pt.height); + } + if (ir.max === pt.x) { + ir.rightSnapIndicator = new tr.ui.SnapIndicator(pt.y, pt.height); + } + }, + + onUpdateTiming(e) { + const pt = this.getSnappedToEventPosition_(e); + this.mouseMoveAt_(pt.x, pt.y, true); + this.updateSnapIndicators_(pt); + }, + + onEndTiming(e) { + this.mouseUp_(); + }, + + onExitTiming(e) { + this.targetElement_.removeEventListener('mousemove', this.onMouseMove_); + this.targetElement_.removeEventListener('dblclick', this.onDblClick_); + }, + + onMouseMove_(e) { + if (e.button) return; + + const worldX = this.getWorldXFromEvent_(e); + this.mouseMoveAt_(worldX, e.clientY, false); + }, + + onDblClick_(e) { + // TODO(nduca): Implement dobuleclicking. + }, + + //////////////////////////////////////////////////////////////////////////// + + isTouchPointInsideTrackBounds_(clientX, clientY) { + if (!this.viewport_ || + !this.viewport_.modelTrackContainer || + !this.viewport_.modelTrackContainer.canvas) { + return false; + } + + const canvas = this.viewport_.modelTrackContainer.canvas; + const canvasRect = canvas.getBoundingClientRect(); + if (clientX >= canvasRect.left && clientX <= canvasRect.right && + clientY >= canvasRect.top && clientY <= canvasRect.bottom) { + return true; + } + + return false; + }, + + mouseDownAt_(worldX, y) { + const ir = this.viewport_.interestRange; + const dt = this.viewport_.currentDisplayTransform; + + const pixelRatio = window.devicePixelRatio || 1; + const nearnessThresholdWorld = dt.xViewVectorToWorld(6 * pixelRatio); + + if (ir.isEmpty) { + ir.setMinAndMax(worldX, worldX); + ir.rightSelected = true; + this.isMovingLeftEdge_ = false; + return; + } + + + // Left edge test. + if (Math.abs(worldX - ir.min) < nearnessThresholdWorld) { + ir.leftSelected = true; + ir.min = worldX; + this.isMovingLeftEdge_ = true; + return; + } + + // Right edge test. + if (Math.abs(worldX - ir.max) < nearnessThresholdWorld) { + ir.rightSelected = true; + ir.max = worldX; + this.isMovingLeftEdge_ = false; + return; + } + + ir.setMinAndMax(worldX, worldX); + ir.rightSelected = true; + this.isMovingLeftEdge_ = false; + }, + + mouseMoveAt_(worldX, y, mouseDown) { + if (mouseDown) { + this.updateMovingEdge_(worldX); + return; + } + + const ir = this.viewport_.interestRange; + const dt = this.viewport_.currentDisplayTransform; + + const pixelRatio = window.devicePixelRatio || 1; + const nearnessThresholdWorld = dt.xViewVectorToWorld(6 * pixelRatio); + + // Left edge test. + if (Math.abs(worldX - ir.min) < nearnessThresholdWorld) { + ir.leftSelected = true; + ir.rightSelected = false; + return; + } + + // Right edge test. + if (Math.abs(worldX - ir.max) < nearnessThresholdWorld) { + ir.leftSelected = false; + ir.rightSelected = true; + return; + } + + ir.leftSelected = false; + ir.rightSelected = false; + return; + }, + + updateMovingEdge_(newWorldX) { + const ir = this.viewport_.interestRange; + let a = ir.min; + let b = ir.max; + if (this.isMovingLeftEdge_) { + a = newWorldX; + } else { + b = newWorldX; + } + + if (a <= b) { + ir.setMinAndMax(a, b); + } else { + ir.setMinAndMax(b, a); + } + + if (ir.min === newWorldX) { + this.isMovingLeftEdge_ = true; + ir.leftSelected = true; + ir.rightSelected = false; + } else { + this.isMovingLeftEdge_ = false; + ir.leftSelected = false; + ir.rightSelected = true; + } + }, + + mouseUp_() { + const dt = this.viewport_.currentDisplayTransform; + const ir = this.viewport_.interestRange; + + ir.leftSelected = false; + ir.rightSelected = false; + + const pixelRatio = window.devicePixelRatio || 1; + const minWidthValue = dt.xViewVectorToWorld(2 * pixelRatio); + if (ir.range < minWidthValue) { + ir.reset(); + } + }, + + getWorldXFromEvent_(e) { + const pixelRatio = window.devicePixelRatio || 1; + const canvas = this.viewport_.modelTrackContainer.canvas; + const worldOffset = canvas.getBoundingClientRect().left; + const viewX = (e.clientX - worldOffset) * pixelRatio; + return this.viewport_.currentDisplayTransform.xViewToWorld(viewX); + }, + + + /** + * Get the closest position of an event within a vertical range of the mouse + * position if possible, otherwise use the position of the mouse pointer. + * @param {MouseEvent} e Mouse event with the current mouse coordinates. + * @return { + * {Number} x, The x coordinate in world space. + * {Number} y, The y coordinate in world space. + * {Number} height, The height of the event. + * {boolean} snapped Whether the coordinates are from a snapped event or + * the mouse position. + * } + */ + getSnappedToEventPosition_(e) { + const pixelRatio = window.devicePixelRatio || 1; + const EVENT_SNAP_RANGE = 16 * pixelRatio; + + const modelTrackContainer = this.viewport_.modelTrackContainer; + const modelTrackContainerRect = + modelTrackContainer.getBoundingClientRect(); + + const viewport = this.viewport_; + const dt = viewport.currentDisplayTransform; + const worldMaxDist = dt.xViewVectorToWorld(EVENT_SNAP_RANGE); + + const worldX = this.getWorldXFromEvent_(e); + const mouseY = e.clientY; + + const selection = new tr.model.EventSet(); + + // Look at the track under mouse position first for better performance. + modelTrackContainer.addClosestEventToSelection( + worldX, worldMaxDist, mouseY, mouseY, selection); + + // Look at all tracks visible on screen. + if (!selection.length) { + modelTrackContainer.addClosestEventToSelection( + worldX, worldMaxDist, + modelTrackContainerRect.top, modelTrackContainerRect.bottom, + selection); + } + + let minDistX = worldMaxDist; + let minDistY = Infinity; + const pixWidth = dt.xViewVectorToWorld(1); + + // Create result object with the mouse coordinates. + const result = { + x: worldX, + y: mouseY - modelTrackContainerRect.top, + height: 0, + snapped: false + }; + + const eventBounds = new tr.b.math.Range(); + for (const event of selection) { + const track = viewport.trackForEvent(event); + const trackRect = track.getBoundingClientRect(); + + eventBounds.reset(); + event.addBoundsToRange(eventBounds); + let eventX; + if (Math.abs(eventBounds.min - worldX) < + Math.abs(eventBounds.max - worldX)) { + eventX = eventBounds.min; + } else { + eventX = eventBounds.max; + } + + const distX = eventX - worldX; + + const eventY = trackRect.top; + const eventHeight = trackRect.height; + const distY = Math.abs(eventY + eventHeight / 2 - mouseY); + + // Prefer events with a closer y position if their x difference is below + // the width of a pixel. + if ((distX <= minDistX || Math.abs(distX - minDistX) < pixWidth) && + distY < minDistY) { + minDistX = distX; + minDistY = distY; + + // Retrieve the event position from the hit. + result.x = eventX; + result.y = eventY + + modelTrackContainer.scrollTop - modelTrackContainerRect.top; + result.height = eventHeight; + result.snapped = true; + } + } + + return result; + } + }; + + return { + TimingTool, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool_test.html new file mode 100644 index 00000000000..c07319d3e97 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool_test.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/timing_tool.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function create100PxWideViewportInto10WideWorld() { + const vp = new tr.ui.TimelineViewport(document.createElement('div')); + const tempDisplayTransform = new tr.ui.TimelineDisplayTransform(); + tempDisplayTransform.xSetWorldBounds(0, 10, 100); + vp.setDisplayTransformImmediately(tempDisplayTransform); + + assert.strictEqual(vp.currentDisplayTransform.xViewToWorld(0), 0); + assert.strictEqual(vp.currentDisplayTransform.xViewToWorld(100), 10); + + return vp; + } + + test('dragLeftInterestRegion', function() { + const vp = create100PxWideViewportInto10WideWorld(); + vp.interestRange.min = 1; + vp.interestRange.max = 9; + const tool = new tr.ui.b.TimingTool(vp); + + tool.mouseDownAt_(1.1, 0); + assert.isTrue(vp.interestRange.leftSelected); + tool.mouseMoveAt_(1.5, 0, true); + assert.strictEqual(vp.interestRange.min, 1.5); + tool.mouseUp_(); + assert.strictEqual(vp.interestRange.min, 1.5); + assert.isFalse(vp.interestRange.leftSelected); + }); + + test('dragRightInterestRegion', function() { + const vp = create100PxWideViewportInto10WideWorld(); + vp.interestRange.min = 1; + vp.interestRange.max = 9; + const tool = new tr.ui.b.TimingTool(vp); + + tool.mouseDownAt_(9.1, 0); + assert.isTrue(vp.interestRange.rightSelected); + tool.mouseMoveAt_(8, 0, true); + assert.strictEqual(vp.interestRange.max, 8); + tool.mouseUp_(); + assert.strictEqual(vp.interestRange.max, 8); + assert.isFalse(vp.interestRange.leftSelected); + }); + + test('dragInNewSpace', function() { + const vp = create100PxWideViewportInto10WideWorld(); + vp.interestRange.min = 1; + vp.interestRange.max = 9; + const tool = new tr.ui.b.TimingTool(vp); + + tool.mouseDownAt_(5, 0); + assert.isTrue(vp.interestRange.rightSelected); + assert.strictEqual(vp.interestRange.min, 5); + assert.strictEqual(vp.interestRange.max, 5); + tool.mouseMoveAt_(4, 0, true); + assert.strictEqual(vp.interestRange.min, 4); + assert.strictEqual(vp.interestRange.max, 5); + assert.isTrue(vp.interestRange.leftSelected); + tool.mouseUp_(); + assert.strictEqual(vp.interestRange.min, 4); + assert.isFalse(vp.interestRange.leftSelected); + assert.isFalse(vp.interestRange.rightSelected); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button.html b/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button.html new file mode 100644 index 00000000000..51108abd247 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +</script> +<dom-module id='tr-ui-b-toolbar-button'> + <template> + <style> + :host { + display: flex; + background-color: #f8f8f8; + border: 1px solid rgba(0, 0, 0, 0.5); + color: rgba(0,0,0,0.8); + justify-content: center; + align-self: stretch; + min-width: 23px; + } + + :host(:hover) { + background-color: rgba(255, 255, 255, 1.0); + border-color: rgba(0, 0, 0, 0.8); + box-shadow: 0 0 .05em rgba(0, 0, 0, 0.4); + color: rgba(0, 0, 0, 1); + } + + #aligner { + display: flex; + flex: 0 0 auto; + align-self: center; + } + </style> + <div id="aligner"> + <slot></slot> + </div> + </template> +</dom-module> +<script> + 'use strict'; + Polymer({ is: 'tr-ui-b-toolbar-button' }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button_test.html new file mode 100644 index 00000000000..d111fb637d8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button_test.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/toolbar_button.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('tallWithTextContent', function() { + const el = document.createElement('tr-ui-b-toolbar-button'); + el.style.width = '100px'; + el.style.height = '40px'; + + Polymer.dom(el).textContent = 'blahblah'; + + this.addHTMLOutput(el); + }); + + test('tallWithInnerSpan', function() { + const el = document.createElement('tr-ui-b-toolbar-button'); + el.style.width = '100px'; + el.style.height = '40px'; + + Polymer.dom(el).appendChild(tr.ui.b.createSpan({textContent: 'blahblah'})); + + this.addHTMLOutput(el); + }); + + test('puny', function() { + const el = document.createElement('tr-ui-b-toolbar-button'); + Polymer.dom(el).textContent = 'M'; + this.addHTMLOutput(el); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/ui.html b/chromium/third_party/catapult/tracing/tracing/ui/base/ui.html new file mode 100644 index 00000000000..a1e647e78b9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/ui.html @@ -0,0 +1,172 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + /** + * Decorates elements as an instance of a class. + * @param {string|!Element} source The way to find the element(s) to decorate. + * If this is a string then {@code querySeletorAll} is used to find the + * elements to decorate. + * @param {!Function} constr The constructor to decorate with. The constr + * needs to have a {@code decorate} function. + */ + function decorate(source, constr) { + let elements; + if (typeof source === 'string') { + elements = Polymer.dom(tr.doc).querySelectorAll(source); + } else { + elements = [source]; + } + + for (let i = 0, el; el = elements[i]; i++) { + if (!(el instanceof constr)) { + constr.decorate(el); + } + } + } + + /** + * Defines a tracing UI component, a function that can be called to construct + * the component. + * + * tr class: + * const List = tr.ui.b.define('list'); + * List.prototype = { + * __proto__: HTMLUListElement.prototype, + * decorate: function() { + * ... + * }, + * ... + * }; + * + * Derived class: + * const CustomList = tr.ui.b.define('custom-list', List); + * CustomList.prototype = { + * __proto__: List.prototype, + * decorate: function() { + * ... + * }, + * ... + * }; + * + * @param {string} className The className of the newly created subtype. If + * subclassing by passing in opt_parentConstructor, this is used for + * debugging. If not subclassing, then it is the tag name that will be + * created by the component. + + * @param {function=} opt_parentConstructor The parent class for this new + * element, if subclassing is desired. If provided, the parent class must + * be also a function created by tr.ui.b.define. + * + * @param {string=} opt_tagNS The namespace in which to create the base + * element. Has no meaning when opt_parentConstructor is passed and must + * either be undefined or the same namespace as the parent class. + * + * @return {function(Object=):Element} The newly created component + * constructor. + */ + function define(className, opt_parentConstructor, opt_tagNS) { + if (typeof className === 'function') { + throw new Error('Passing functions as className is deprecated. Please ' + + 'use (className, opt_parentConstructor) to subclass'); + } + + className = className.toLowerCase(); + if (opt_parentConstructor && !opt_parentConstructor.tagName) { + throw new Error('opt_parentConstructor was not ' + + 'created by tr.ui.b.define'); + } + + // Walk up the parent constructors until we can find the type of tag + // to create. + let tagName = className; + let tagNS = undefined; + if (opt_parentConstructor) { + if (opt_tagNS) { + throw new Error('Must not specify tagNS if parentConstructor is given'); + } + let parent = opt_parentConstructor; + while (parent && parent.tagName) { + tagName = parent.tagName; + tagNS = parent.tagNS; + parent = parent.parentConstructor; + } + } else { + tagNS = opt_tagNS; + } + + /** + * Creates a new UI element constructor. + * Arguments passed to the constuctor are provided to the decorate method. + * You will need to call the parent elements decorate method from within + * your decorate method and pass any required parameters. + * @constructor + */ + function f() { + if (opt_parentConstructor && + f.prototype.__proto__ !== opt_parentConstructor.prototype) { + throw new Error( + className + ' prototye\'s __proto__ field is messed up. ' + + 'It MUST be the prototype of ' + opt_parentConstructor.tagName); + } + + let el; + if (tagNS === undefined) { + el = tr.doc.createElement(tagName); + } else { + el = tr.doc.createElementNS(tagNS, tagName); + } + f.decorate.call(this, el, arguments); + return el; + } + + /** + * Decorates an element as a UI element class. + * @param {!Element} el The element to decorate. + */ + f.decorate = function(el) { + el.__proto__ = f.prototype; + el.decorate.apply(el, arguments[1]); + el.constructor = f; + }; + + f.className = className; + f.tagName = tagName; + f.tagNS = tagNS; + f.parentConstructor = (opt_parentConstructor ? opt_parentConstructor : + undefined); + f.toString = function() { + if (!f.parentConstructor) { + return f.tagName; + } + return f.parentConstructor.toString() + '::' + f.className; + }; + + return f; + } + + function elementIsChildOf(el, potentialParent) { + if (el === potentialParent) return false; + + let cur = el; + while (Polymer.dom(cur).parentNode) { + if (cur === potentialParent) return true; + cur = Polymer.dom(cur).parentNode; + } + return false; + } + + return { + decorate, + define, + elementIsChildOf, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state.html b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state.html new file mode 100644 index 00000000000..c87041eeb45 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/location.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const Location = tr.model.Location; + + /** + * UIState is a class that represents the current state of the timeline by + * the Location of the point of interest and the current scaleX of the + * timeline. + * + * @constructor + */ + function UIState(location, scaleX) { + this.location_ = location; + this.scaleX_ = scaleX; + } + + /** + * Accepts a UIState string in the format of (timestamp)@(stableID)x(scaleX) + * Returns undefined if string is not in this format, or throws an Error if + * variables in a syntactically-correct stateString does not produce a valid + * UIState. Otherwise returns a constructed UIState instance. + */ + UIState.fromUserFriendlyString = function(model, viewport, stateString) { + const navByFinderPattern = /^(-?\d+(\.\d+)?)@(.+)x(\d+(\.\d+)?)$/g; + const match = navByFinderPattern.exec(stateString); + if (!match) return; + + const timestamp = parseFloat(match[1]); + const stableId = match[3]; + const scaleX = parseFloat(match[4]); + + if (scaleX <= 0) { + throw new Error('Invalid ScaleX value in UI State string.'); + } + + if (!viewport.containerToTrackMap.getTrackByStableId(stableId)) { + throw new Error('Invalid StableID given in UI State String.'); + } + + const loc = tr.model.Location.fromStableIdAndTimestamp( + viewport, stableId, timestamp); + return new UIState(loc, scaleX); + }; + + UIState.prototype = { + + get location() { + return this.location_; + }, + + get scaleX() { + return this.scaleX_; + }, + + toUserFriendlyString(viewport) { + const timestamp = this.location_.xWorld; + const stableId = + this.location_.getContainingTrack(viewport).eventContainer.stableId; + const scaleX = this.scaleX_; + return timestamp.toFixed(5) + '@' + stableId + 'x' + scaleX.toFixed(5); + }, + + toDict() { + return { + location: this.location_.toDict(), + scaleX: this.scaleX_ + }; + } + }; + + return { + UIState, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state_test.html new file mode 100644 index 00000000000..de9c6ba6090 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state_test.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/base/ui_state.html"> +<link rel="import" href="/tracing/ui/tracks/track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const UIState = tr.ui.b.UIState; + + function FakeModel() { + this.processes = { 1: { threads: { 2: { stableId: '1.2' } } } }; + } + + // FakeTrack needs to be an instance of tr.ui.tracks.Track because a + // location is constructed in terms of Track instances. + function FakeTrack() { } + FakeTrack.prototype = { + __proto__: tr.ui.tracks.Track.prototype, + + get eventContainer() { + return { stableId: '1.2' }; + }, + + getBoundingClientRect() { + return { top: 5, height: 2 }; + }, + + get parentElement() { + return null; + } + }; + + function FakeViewPort() { + this.containerToTrackMap = { + // "1.2" is the only valid stableId this test function accepts. + getTrackByStableId(stableId) { + if (stableId === '1.2') { + return new FakeTrack; + } + return undefined; + } + }; + } + + test('invalidStableId', function() { + const model = new FakeModel; + const vp = new FakeViewPort; + assert.throws(function() { + UIState.fromUserFriendlyString(model, vp, '15@1.3x6'); + }); + assert.throws(function() { + UIState.fromUserFriendlyString(model, vp, '15@2.2x6'); + }); + assert.throws(function() { + UIState.fromUserFriendlyString(model, vp, '505@1.x5'); + }); + }); + + test('invalidScaleX', function() { + const model = new FakeModel; + const vp = new FakeViewPort; + assert.isUndefined(UIState.fromUserFriendlyString(model, vp, '1@1.2x-1')); + assert.throws(function() { + UIState.fromUserFriendlyString(model, vp, '1@1.2x0'); + }); + }); + + test('invalidSyntax', function() { + const model = new FakeModel; + const vp = new FakeViewPort; + assert.isUndefined(UIState.fromUserFriendlyString(model, vp, '5')); + assert.isUndefined(UIState.fromUserFriendlyString(model, vp, '5@x5')); + assert.isUndefined(UIState.fromUserFriendlyString(model, vp, 'ab@1.2x5')); + }); + + test('validString', function() { + const model = new FakeModel; + const vp = new FakeViewPort; + const str = '-50125.51231@1.2x1.12345'; + const uiState = UIState.fromUserFriendlyString(model, vp, str); + + assert.isDefined(uiState); + assert.strictEqual(uiState.location.xWorld, -50125.51231); + assert.strictEqual( + uiState.location.getContainingTrack(vp).eventContainer.stableId, + '1.2'); + assert.strictEqual(uiState.scaleX, 1.12345); + + assert.strictEqual(uiState.toUserFriendlyString(vp), str); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/ui_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_test.html new file mode 100644 index 00000000000..486c1019c62 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_test.html @@ -0,0 +1,246 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/ui.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const TestElement = tr.ui.b.define('div'); + TestElement.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + if (!this.decorateCallCount) { + this.decorateCallCount = 0; + } + this.decorateCallCount++; + } + }; + + const Base = tr.ui.b.define('div'); + Base.prototype = { + __proto__: HTMLDivElement.prototype, + decorate() { + this.decoratedAsBase = true; + }, + set baseProperty(v) { + this.basePropertySet = v; + } + }; + + test('decorateOnceViaNew', function() { + const testElement = new TestElement(); + assert.strictEqual(testElement.decorateCallCount, 1); + }); + + test('decorateOnceDirectly', function() { + const testElement = document.createElement('div'); + tr.ui.b.decorate(testElement, TestElement); + assert.strictEqual(testElement.decorateCallCount, 1); + }); + + test('componentToString', function() { + assert.strictEqual(Base.toString(), 'div'); + + const Sub = tr.ui.b.define('Sub', Base); + assert.strictEqual(Sub.toString(), 'div::sub'); + + const SubSub = tr.ui.b.define('Marine', Sub); + assert.strictEqual(SubSub.toString(), 'div::sub::marine'); + }); + + test('basicDefines', function() { + const baseInstance = new Base(); + assert.instanceOf(baseInstance, Base); + assert.isTrue(baseInstance.decoratedAsBase); + + assert.strictEqual(baseInstance.constructor, Base); + assert.strictEqual(baseInstance.constructor.toString(), 'div'); + + baseInstance.basePropertySet = 7; + assert.strictEqual(baseInstance.basePropertySet, 7); + }); + + test('subclassing', function() { + const Sub = tr.ui.b.define('sub', Base); + Sub.prototype = { + __proto__: Base.prototype, + decorate() { + this.decoratedAsSub = true; + } + }; + + const subInstance = new Sub(); + assert.instanceOf(subInstance, Sub); + assert.isTrue(subInstance.decoratedAsSub); + + assert.instanceOf(subInstance, Base); + assert.isUndefined(subInstance.decoratedAsBase); + + assert.strictEqual(subInstance.constructor, Sub); + assert.strictEqual(subInstance.constructor.toString(), 'div::sub'); + + subInstance.baseProperty = true; + assert.isTrue(subInstance.basePropertySet); + }); + + const NoArgs = tr.ui.b.define('div'); + NoArgs.prototype = { + __proto__: HTMLDivElement.prototype, + decorate() { + this.noArgsDecorated_ = true; + }, + get noArgsDecorated() { + return this.noArgsDecorated_; + } + }; + + const Args = tr.ui.b.define('args', NoArgs); + Args.prototype = { + __proto__: NoArgs.prototype, + decorate(first) { + this.first_ = first; + this.argsDecorated_ = true; + }, + get first() { + return this.first_; + }, + get argsDecorated() { + return this.argsDecorated_; + } + }; + + const ArgsChild = tr.ui.b.define('args-child', Args); + ArgsChild.prototype = { + __proto__: Args.prototype, + decorate(_, second) { + this.second_ = second; + this.argsChildDecorated_ = true; + }, + get second() { + return this.second_; + }, + get decorated() { + return this.decorated_; + }, + get argsChildDecorated() { + return this.argsChildDecorated_ = true; + } + }; + + const ArgsDecoratingChild = tr.ui.b.define('args-decorating-child', Args); + ArgsDecoratingChild.prototype = { + __proto__: Args.prototype, + decorate(first, second) { + Args.prototype.decorate.call(this, first); + this.second_ = second; + this.argsDecoratingChildDecorated_ = true; + }, + get second() { + return this.second_; + }, + get decorated() { + return this.decorated_; + }, + get argsDecoratingChildDecorated() { + return this.argsChildDecorated_ = true; + } + }; + + test('decorate_noArguments', function() { + let noArgs; + assert.doesNotThrow(function() { + noArgs = new NoArgs(); + }); + assert.isTrue(noArgs.noArgsDecorated); + }); + + test('decorate_arguments', function() { + const args = new Args('this is first'); + assert.strictEqual(args.first, 'this is first'); + assert.isTrue(args.argsDecorated); + assert.isUndefined(args.noArgsDecorated); + }); + + test('decorate_subclassArguments', function() { + const argsChild = new ArgsChild('this is first', 'and second'); + assert.isUndefined(argsChild.first); + assert.strictEqual(argsChild.second, 'and second'); + + assert.isTrue(argsChild.argsChildDecorated); + assert.isUndefined(argsChild.argsDecorated); + assert.isUndefined(argsChild.noArgsDecorated); + }); + + test('decorate_subClassCallsParentDecorate', function() { + const argsDecoratingChild = new ArgsDecoratingChild( + 'this is first', 'and second'); + assert.strictEqual(argsDecoratingChild.first, 'this is first'); + assert.strictEqual(argsDecoratingChild.second, 'and second'); + assert.isTrue(argsDecoratingChild.argsDecoratingChildDecorated); + assert.isTrue(argsDecoratingChild.argsDecorated); + assert.isUndefined(argsDecoratingChild.noArgsDecorated); + }); + + test('defineWithNamespace', function() { + const svgNS = 'http://www.w3.org/2000/svg'; + const cls = tr.ui.b.define('svg', undefined, svgNS); + cls.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + Polymer.dom(this).setAttribute('width', 200); + Polymer.dom(this).setAttribute('height', 200); + Polymer.dom(this).setAttribute('viewPort', '0 0 200 200'); + const rectEl = document.createElementNS(svgNS, 'rect'); + Polymer.dom(rectEl).setAttribute('x', 10); + Polymer.dom(rectEl).setAttribute('y', 10); + Polymer.dom(rectEl).setAttribute('width', 180); + Polymer.dom(rectEl).setAttribute('height', 180); + Polymer.dom(this).appendChild(rectEl); + } + }; + const el = new cls(); + assert.strictEqual(el.tagName, 'svg'); + assert.strictEqual(el.namespaceURI, svgNS); + this.addHTMLOutput(el); + }); + + test('defineSubclassWithNamespace', function() { + const svgNS = 'http://www.w3.org/2000/svg'; + const cls = tr.ui.b.define('svg', undefined, svgNS); + cls.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + Polymer.dom(this).setAttribute('width', 200); + Polymer.dom(this).setAttribute('height', 200); + Polymer.dom(this).setAttribute('viewPort', '0 0 200 200'); + const rectEl = document.createElementNS(svgNS, 'rect'); + Polymer.dom(rectEl).setAttribute('x', 10); + Polymer.dom(rectEl).setAttribute('y', 10); + Polymer.dom(rectEl).setAttribute('width', 180); + Polymer.dom(rectEl).setAttribute('height', 180); + Polymer.dom(this).appendChild(rectEl); + } + }; + + const subCls = tr.ui.b.define('sub', cls); + subCls.prototype = { + __proto__: cls.prototype + }; + assert.strictEqual(subCls.toString(), 'svg::sub'); + + const el = new subCls(); + this.addHTMLOutput(el); + assert.strictEqual(el.tagName, 'svg'); + assert.strictEqual(el.namespaceURI, svgNS); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/utils.html b/chromium/third_party/catapult/tracing/tracing/ui/base/utils.html new file mode 100644 index 00000000000..cca1003cc06 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/utils.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/math/rect.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + function instantiateTemplate(selector, doc) { + doc = doc || document; + const el = Polymer.dom(doc).querySelector(selector); + if (!el) { + throw new Error('Element not found: ' + selector); + } + return doc.importNode(el.content, true); + // return el.createInstance(); + } + + function windowRectForElement(element) { + const position = [element.offsetLeft, element.offsetTop]; + const size = [element.offsetWidth, element.offsetHeight]; + let node = element.offsetParent; + while (node) { + position[0] += node.offsetLeft; + position[1] += node.offsetTop; + node = node.offsetParent; + } + return tr.b.math.Rect.fromXYWH(position[0], position[1], size[0], size[1]); + } + + function scrollIntoViewIfNeeded(el) { + const pr = el.parentElement.getBoundingClientRect(); + const cr = el.getBoundingClientRect(); + if (cr.top < pr.top) { + el.scrollIntoView(true); + } else if (cr.bottom > pr.bottom) { + el.scrollIntoView(false); + } + } + + function extractUrlString(url) { + let extracted = url.replace(/url\((.*)\)/, '$1'); + + // In newer versions of chrome, the contents of url() will be quoted. Remove + // these quotes as well. If quotes are not present, match will fail and this + // becomes a no-op. + extracted = extracted.replace(/\"(.*)\"/, '$1'); + + return extracted; + } + + function toThreeDigitLocaleString(value) { + return value.toLocaleString( + undefined, {minimumFractionDigits: 3, maximumFractionDigits: 3}); + } + + /** + * Returns true if |name| is the name of an unknown HTML element. Registered + * polymer elements are known, so this returns false. Typos of registered + * polymer element names are unknown, so this returns true for typos. + * + * @return {boolean} + */ + function isUnknownElementName(name) { + return document.createElement(name) instanceof HTMLUnknownElement; + } + + return { + isUnknownElementName, + toThreeDigitLocaleString, + instantiateTemplate, + windowRectForElement, + scrollIntoViewIfNeeded, + extractUrlString, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/utils_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/utils_test.html new file mode 100644 index 00000000000..4ab76e1d221 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/base/utils_test.html @@ -0,0 +1,77 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/utils.html"> + +<dom-module id='instantiate-template-polymer-element-test'> + <template></template> +</dom-module> +<script> +'use strict'; +Polymer({ + is: 'instantiate-template-polymer-element-test', + testProperty: 'Test' +}); +</script> +<template id="instantiate-template-polymer-test"> + <instantiate-template-polymer-element-test> + </instantiate-template-polymer-element-test> +</template> + +<template id="multiple-template-test"> + <template> + <instantiate-template-polymer-element-test> + </instantiate-template-polymer-element-test> + <span test-attribute='TestAttribute'>Foo</span> + </template> + <instantiate-template-polymer-element-test> + </instantiate-template-polymer-element-test> +</template> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const THIS_DOC = document.currentScript.ownerDocument; + + test('instantiateTemplatePolymer', function() { + const e = tr.ui.b.instantiateTemplate( + '#instantiate-template-polymer-test', + THIS_DOC); + assert.strictEqual(e.children.length, 1); + assert.strictEqual(e.children[0].testProperty, 'Test'); + }); + + test('instantiateTemplateMultipleTemplates', function() { + const outerElement = tr.ui.b.instantiateTemplate( + '#multiple-template-test', + THIS_DOC); + assert.strictEqual(outerElement.children.length, 2); + assert.strictEqual(outerElement.children[1].testProperty, 'Test'); + + // Make sure we can still instantiate inner templates, if we need them. + const innerElement = THIS_DOC.importNode( + outerElement.children[0].content, true); + assert.strictEqual(innerElement.children.length, 2); + assert.strictEqual(innerElement.children[0].testProperty, 'Test'); + assert.strictEqual( + innerElement.children[1].getAttribute('test-attribute'), + 'TestAttribute'); + assert.strictEqual( + Polymer.dom(innerElement.children[1]).textContent, 'Foo'); + }); + + test('extractUrlStringAcceptsBothVersions', function() { + const oldStyleUrl = 'url(content)'; + const newStyleUrl = 'url("content")'; + const expectedResult = 'content'; + + assert.strictEqual(tr.ui.b.extractUrlString(oldStyleUrl), expectedResult); + assert.strictEqual(tr.ui.b.extractUrlString(newStyleUrl), expectedResult); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/brushing_state.html b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state.html new file mode 100644 index 00000000000..f88e7a9bca3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state.html @@ -0,0 +1,280 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/guid.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/selection_state.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.b', function() { + const EventSet = tr.model.EventSet; + const SelectionState = tr.model.SelectionState; + + function BrushingState() { + this.guid_ = tr.b.GUID.allocateSimple(); + this.selection_ = new EventSet(); + this.findMatches_ = new EventSet(); + this.analysisViewRelatedEvents_ = new EventSet(); + this.analysisLinkHoveredEvents_ = new EventSet(); + this.appliedToModel_ = undefined; + this.viewSpecificBrushingStates_ = {}; + } + BrushingState.prototype = { + get guid() { + return this.guid_; + }, + + clone() { + const that = new BrushingState(); + that.selection_ = this.selection_; + that.findMatches_ = this.findMatches_; + that.analysisViewRelatedEvents_ = this.analysisViewRelatedEvents_; + that.analysisLinkHoveredEvents_ = this.analysisLinkHoveredEvents_; + that.viewSpecificBrushingStates_ = this.viewSpecificBrushingStates_; + + return that; + }, + + equals(that) { + if (!this.selection_.equals(that.selection_)) { + return false; + } + if (!this.findMatches_.equals(that.findMatches_)) { + return false; + } + if (!this.analysisViewRelatedEvents_.equals( + that.analysisViewRelatedEvents_)) { + return false; + } + if (!this.analysisLinkHoveredEvents_.equals( + that.analysisLinkHoveredEvents_)) { + return false; + } + // We currently do not take the view-specific brushing states into + // account. If we did, every change of the view-specific brushing state + // of any view would cause a redraw of the whole UI (see the + // BrushingStateController.currentBrushingState setter). + return true; + }, + + get selectionOfInterest() { + if (this.selection_.length) { + return this.selection_; + } + + if (this.highlight_.length) { + return this.highlight_; + } + + if (this.analysisViewRelatedEvents_.length) { + return this.analysisViewRelatedEvents_; + } + + if (this.analysisLinkHoveredEvents_.length) { + return this.analysisLinkHoveredEvents_; + } + + return this.selection_; + }, + + get selection() { + return this.selection_; + }, + + set selection(selection) { + if (this.appliedToModel_) { + throw new Error('Cannot mutate this state right now'); + } + if (selection === undefined) { + selection = new EventSet(); + } + this.selection_ = selection; + }, + + get findMatches() { + return this.findMatches_; + }, + + set findMatches(findMatches) { + if (this.appliedToModel_) { + throw new Error('Cannot mutate this state right now'); + } + if (findMatches === undefined) { + findMatches = new EventSet(); + } + this.findMatches_ = findMatches; + }, + + get analysisViewRelatedEvents() { + return this.analysisViewRelatedEvents_; + }, + + set analysisViewRelatedEvents(analysisViewRelatedEvents) { + if (this.appliedToModel_) { + throw new Error('Cannot mutate this state right now'); + } + if (!(analysisViewRelatedEvents instanceof EventSet)) { + analysisViewRelatedEvents = new EventSet(); + } + this.analysisViewRelatedEvents_ = analysisViewRelatedEvents; + }, + + get analysisLinkHoveredEvents() { + return this.analysisLinkHoveredEvents_; + }, + + set analysisLinkHoveredEvents(analysisLinkHoveredEvents) { + if (this.appliedToModel_) { + throw new Error('Cannot mutate this state right now'); + } + if (!(analysisLinkHoveredEvents instanceof EventSet)) { + analysisLinkHoveredEvents = new EventSet(); + } + this.analysisLinkHoveredEvents_ = analysisLinkHoveredEvents; + }, + + get isAppliedToModel() { + return this.appliedToModel_ !== undefined; + }, + + get viewSpecificBrushingStates() { + return this.viewSpecificBrushingStates_; + }, + + set viewSpecificBrushingStates(viewSpecificBrushingStates) { + this.viewSpecificBrushingStates_ = viewSpecificBrushingStates; + }, + + get dimmedEvents_() { + const dimmedEvents = new EventSet(); + dimmedEvents.addEventSet(this.findMatches); + dimmedEvents.addEventSet(this.analysisViewRelatedEvents_); + return dimmedEvents; + }, + + get brightenedEvents_() { + const brightenedEvents = new EventSet(); + brightenedEvents.addEventSet(this.selection_); + brightenedEvents.addEventSet(this.analysisLinkHoveredEvents_); + return brightenedEvents; + }, + + /** + * This function sets the SelectionStates according to these rules: + * + * - Events in ONE of findMatches or analysisViewRelatedEvents + * are set to SelectionState.BRIGHTENED0. + * - Events in BOTH of findMatches and analysisViewRelatedEvents + * are set to SelectionState.BRIGHTENED1. + * - Events in ONE of selection or analysisLinkHoveredEvents + * are set to SelectionState.DIMMED1. + * - Events in BOTH selection and analysisLinkHoveredEvents + * are set to SelectionState.DIMMED2. + * - Events not in any of the above are set to SelectionState.NONE + * if there are no events in selection or analysisLinkHoveredEvents + * (i.e. model is "default bright") or SelectionState.DIMMED0 (i.e. + * model is "default dimmed"). + * + * It is up to the caller to assure that all of the SelectionStates + * are the same before calling this function. Normally, + * this is done by calling unapplyFromModelSelectionState on the + * old brushing state first. + */ + applyToEventSelectionStates(model) { + this.appliedToModel_ = model; + + const dimmedEvents = this.dimmedEvents_; + + // It's possible for this to get called with an undefined model pointer. + // If so, skip adjusting the defaults. + if (model) { + const newDefaultState = ( + dimmedEvents.length ? SelectionState.DIMMED0 : SelectionState.NONE); + + // Since all the states are the same, we can get the current default + // state by looking at the first element. + const currentDefaultState = tr.b.getFirstElement( + model.getDescendantEvents()).selectionState; + + // If the default state was changed, then we have to iterate through + // and reset all the events to the new default state. + if (currentDefaultState !== newDefaultState) { + for (const e of model.getDescendantEvents()) { + e.selectionState = newDefaultState; + } + } + } + + // Now we apply the other rules above. + let score; + for (const e of dimmedEvents) { + score = 0; + if (this.findMatches_.contains(e)) { + score++; + } + if (this.analysisViewRelatedEvents_.contains(e)) { + score++; + } + e.selectionState = SelectionState.getFromDimmingLevel(score); + } + + for (const e of this.brightenedEvents_) { + score = 0; + if (this.selection_.contains(e)) { + score++; + } + if (this.analysisLinkHoveredEvents_.contains(e)) { + score++; + } + e.selectionState = SelectionState.getFromBrighteningLevel(score); + } + }, + + transferModelOwnershipToClone(that) { + if (!this.appliedToModel_) { + throw new Error('Not applied'); + } + // Assumes this.equals(that). + that.appliedToModel_ = this.appliedToModel_; + this.appliedToModel_ = undefined; + }, + + /** + * Unapplies this brushing state from the model selection state. + * Resets all the SelectionStates to their default value (DIMMED0 or NONE) + * and returns the default selection states. The caller should store this + * value and pass it into applyFromModelSelectionStat when that is called. + */ + unapplyFromEventSelectionStates() { + if (!this.appliedToModel_) { + throw new Error('Not applied'); + } + const model = this.appliedToModel_; + this.appliedToModel_ = undefined; + + const dimmedEvents = this.dimmedEvents_; + const defaultState = ( + dimmedEvents.length ? SelectionState.DIMMED0 : SelectionState.NONE); + + for (const e of this.brightenedEvents_) { + e.selectionState = defaultState; + } + for (const e of dimmedEvents) { + e.selectionState = defaultState; + } + return defaultState; + } + }; + + return { + BrushingState, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller.html new file mode 100644 index 00000000000..6a547c339c3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller.html @@ -0,0 +1,317 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/event_target.html"> +<link rel="import" href="/tracing/base/task.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/selection_state.html"> +<link rel="import" href="/tracing/ui/base/ui_state.html"> +<link rel="import" href="/tracing/ui/brushing_state.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> + +<script> +'use strict'; + +tr.exportTo('tr.c', function() { + const BrushingState = tr.ui.b.BrushingState; + const EventSet = tr.model.EventSet; + const SelectionState = tr.model.SelectionState; + const Viewport = tr.ui.TimelineViewport; + + function BrushingStateController(timelineView) { + tr.b.EventTarget.call(this); + + this.timelineView_ = timelineView; + this.currentBrushingState_ = new BrushingState(); + + this.onPopState_ = this.onPopState_.bind(this); + this.historyEnabled_ = false; + this.selections_ = {}; + } + + BrushingStateController.prototype = { + __proto__: tr.b.EventTarget.prototype, + + dispatchChangeEvent_() { + const e = new tr.b.Event('change', false, false); + this.dispatchEvent(e); + }, + + get model() { + if (!this.timelineView_) { + return undefined; + } + return this.timelineView_.model; + }, + + get trackView() { + if (!this.timelineView_) { + return undefined; + } + return this.timelineView_.trackView; + }, + + get viewport() { + if (!this.timelineView_) { + return undefined; + } + if (!this.timelineView_.trackView) { + return undefined; + } + return this.timelineView_.trackView.viewport; + }, + + /* History system */ + get historyEnabled() { + return this.historyEnabled_; + }, + + set historyEnabled(historyEnabled) { + this.historyEnabled_ = !!historyEnabled; + if (historyEnabled) { + window.addEventListener('popstate', this.onPopState_); + } else { + window.removeEventListener('popstate', this.onPopState_); + } + }, + + modelWillChange() { + if (this.currentBrushingState_.isAppliedToModel) { + this.currentBrushingState_.unapplyFromEventSelectionStates(); + } + }, + + modelDidChange() { + this.selections_ = {}; + + this.currentBrushingState_ = new BrushingState(); + this.currentBrushingState_.applyToEventSelectionStates(this.model); + + const e = new tr.b.Event('model-changed', false, false); + this.dispatchEvent(e); + + this.dispatchChangeEvent_(); + }, + + onUserInitiatedSelectionChange_() { + const selection = this.selection; + if (this.historyEnabled) { + // Save the selection so that when back button is pressed, + // it could be retrieved. + this.selections_[selection.guid] = selection; + const state = { + selection_guid: selection.guid + }; + + window.history.pushState(state, document.title); + } + }, + + onPopState_(e) { + if (e.state === null) return; + + const selection = this.selections_[e.state.selection_guid]; + if (selection) { + const newState = this.currentBrushingState_.clone(); + newState.selection = selection; + this.currentBrushingState = newState; + } + e.stopPropagation(); + }, + + get selection() { + return this.currentBrushingState_.selection; + }, + get findMatches() { + return this.currentBrushingState_.findMatches; + }, + + get selectionOfInterest() { + return this.currentBrushingState_.selectionOfInterest; + }, + + get currentBrushingState() { + return this.currentBrushingState_; + }, + + set currentBrushingState(newBrushingState) { + if (newBrushingState.isAppliedToModel) { + throw new Error('Cannot apply this state, it is applied'); + } + + // This function uses value-equality on the states so that state can + // changed to a clone of itself without causing a change event, while + // still having the actual state object change to the new clone. + const hasValueChanged = !this.currentBrushingState_.equals( + newBrushingState); + + if (newBrushingState !== this.currentBrushingState_ && !hasValueChanged) { + if (this.currentBrushingState_.isAppliedToModel) { + this.currentBrushingState_.transferModelOwnershipToClone( + newBrushingState); + } + this.currentBrushingState_ = newBrushingState; + return; + } + + if (this.currentBrushingState_.isAppliedToModel) { + this.currentBrushingState_.unapplyFromEventSelectionStates(); + } + + this.currentBrushingState_ = newBrushingState; + + this.currentBrushingState_.applyToEventSelectionStates(this.model); + + this.dispatchChangeEvent_(); + }, + + /** + * @param {Filter} filter The filter to use for finding matches. + * @param {Selection} selection The selection to add matches to. + * @return {Task} which performs the filtering. + */ + addAllEventsMatchingFilterToSelectionAsTask(filter, selection) { + const timelineView = this.timelineView_.trackView; + if (!timelineView) { + return new tr.b.Task(); + } + return timelineView.addAllEventsMatchingFilterToSelectionAsTask( + filter, selection); + }, + + findTextChangedTo(allPossibleMatches) { + const newBrushingState = this.currentBrushingState_.clone(); + newBrushingState.findMatches = allPossibleMatches; + this.currentBrushingState = newBrushingState; + }, + + findFocusChangedTo(currentFocus) { + const newBrushingState = this.currentBrushingState_.clone(); + newBrushingState.selection = currentFocus; + this.currentBrushingState = newBrushingState; + + this.onUserInitiatedSelectionChange_(); + }, + + findTextCleared() { + if (this.xNavStringMarker_ !== undefined) { + this.model.removeAnnotation(this.xNavStringMarker_); + this.xNavStringMarker_ = undefined; + } + + if (this.guideLineAnnotation_ !== undefined) { + this.model.removeAnnotation(this.guideLineAnnotation_); + this.guideLineAnnotation_ = undefined; + } + + const newBrushingState = this.currentBrushingState_.clone(); + newBrushingState.selection = new EventSet(); + newBrushingState.findMatches = new EventSet(); + this.currentBrushingState = newBrushingState; + + this.onUserInitiatedSelectionChange_(); + }, + + uiStateFromString(string) { + return tr.ui.b.UIState.fromUserFriendlyString( + this.model, this.viewport, string); + }, + + navToPosition(uiState, showNavLine) { + this.trackView.navToPosition(uiState, showNavLine); + }, + + changeSelectionFromTimeline(selection) { + const newBrushingState = this.currentBrushingState_.clone(); + newBrushingState.selection = selection; + newBrushingState.findMatches = new EventSet(); + this.currentBrushingState = newBrushingState; + + this.onUserInitiatedSelectionChange_(); + }, + + showScriptControlSelection(selection) { + const newBrushingState = this.currentBrushingState_.clone(); + newBrushingState.selection = selection; + newBrushingState.findMatches = new EventSet(); + this.currentBrushingState = newBrushingState; + }, + + changeSelectionFromRequestSelectionChangeEvent(selection) { + const newBrushingState = this.currentBrushingState_.clone(); + newBrushingState.selection = selection; + newBrushingState.findMatches = new EventSet(); + this.currentBrushingState = newBrushingState; + + this.onUserInitiatedSelectionChange_(); + }, + + changeAnalysisViewRelatedEvents(eventSet) { + const newBrushingState = this.currentBrushingState_.clone(); + newBrushingState.analysisViewRelatedEvents = eventSet; + this.currentBrushingState = newBrushingState; + }, + + changeAnalysisLinkHoveredEvents(eventSet) { + const newBrushingState = this.currentBrushingState_.clone(); + newBrushingState.analysisLinkHoveredEvents = eventSet; + this.currentBrushingState = newBrushingState; + }, + + getViewSpecificBrushingState(viewId) { + return this.currentBrushingState.viewSpecificBrushingStates[viewId]; + }, + + changeViewSpecificBrushingState(viewId, newState) { + const oldStates = this.currentBrushingState_.viewSpecificBrushingStates; + const newStates = {}; + for (const id in oldStates) { + newStates[id] = oldStates[id]; + } + if (newState === undefined) { + delete newStates[viewId]; + } else { + newStates[viewId] = newState; + } + + const newBrushingState = this.currentBrushingState_.clone(); + newBrushingState.viewSpecificBrushingStates = newStates; + this.currentBrushingState = newBrushingState; + } + }; + + BrushingStateController.getControllerForElement = function(element) { + if (tr.isHeadless) { + throw new Error('Unsupported'); + } + let currentElement = element; + while (currentElement) { + if (currentElement.brushingStateController) { + return currentElement.brushingStateController; + } + + // Walk up the DOM. + if (currentElement.parentElement) { + currentElement = currentElement.parentElement; + continue; + } + + // Possibly inside a shadow DOM. + let currentNode = currentElement; + while (Polymer.dom(currentNode).parentNode) { + currentNode = Polymer.dom(currentNode).parentNode; + } + currentElement = currentNode.host; + } + return undefined; + }; + + return { + BrushingStateController, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller_test.html new file mode 100644 index 00000000000..b72aeccf16a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller_test.html @@ -0,0 +1,204 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/task.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/brushing_state_controller.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + + const EventSet = tr.model.EventSet; + const SelectionState = tr.model.SelectionState; + const Task = tr.b.Task; + + function newSimpleFakeTimelineView() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + + m.sA = m.t2.sliceGroup.pushSlice( + newSliceEx({title: 'a', start: 0, end: 5})); + m.sB = m.t2.sliceGroup.pushSlice( + newSliceEx({title: 'b', start: 10, end: 15})); + m.sC = m.t2.sliceGroup.pushSlice( + newSliceEx({title: 'c', start: 20, end: 20})); + }); + + // Fake timeline view. So fake its ... just wow. + const timelineView = { + model: m + }; + return timelineView; + } + + function doesCauseChangeToFire(brushingStateController, cb, opt_this) { + let didFire = false; + function didFireCb() { + didFire = true; + } + brushingStateController.addEventListener('change', didFireCb); + cb.call(opt_this); + brushingStateController.removeEventListener('change', didFireCb); + return didFire; + } + + test('simpleStateChanges', function() { + const timelineView = newSimpleFakeTimelineView(); + const brushingStateController = + new tr.c.BrushingStateController(timelineView); + const m = timelineView.model; + + // Setting empty brushing state doesn't cause change event. This behavior + // is triggered when the user tries to search for something when no trace + // has been loaded yet in chrome://tracing. + const bs0 = new tr.ui.b.BrushingState(); + assert.isFalse(doesCauseChangeToFire( + brushingStateController, + function() { + brushingStateController.currentBrushingState = bs0; + })); + assert.isFalse(bs0.isAppliedToModel); + assert.strictEqual(brushingStateController.currentBrushingState, bs0); + + // Setting causes change. + const bs1 = new tr.ui.b.BrushingState(); + bs1.selection = new EventSet([m.sA]); + assert.isTrue(doesCauseChangeToFire( + brushingStateController, + function() { + brushingStateController.currentBrushingState = bs1; + })); + assert.isTrue(bs1.isAppliedToModel); + + // Setting value equivalent doesn't cause change event. + const bs2 = bs1.clone(); + assert.isFalse(doesCauseChangeToFire( + brushingStateController, + function() { + brushingStateController.currentBrushingState = bs2; + })); + assert.strictEqual(brushingStateController.currentBrushingState, bs2); + assert.isTrue( + brushingStateController.currentBrushingState.isAppliedToModel); + + // Setting to something different unapplies the old bs. + const bs3 = new tr.ui.b.BrushingState(); + bs3.findMatches = new EventSet([m.sA, m.sB]); + brushingStateController.currentBrushingState = bs3; + assert.isTrue(bs3.isAppliedToModel); + assert.isFalse(bs2.isAppliedToModel); + }); + + test('modelCausesStateChange', function() { + const timelineView = newSimpleFakeTimelineView(); + const brushingStateController = + new tr.c.BrushingStateController(timelineView); + + const m1 = timelineView.model; + + const bs1 = new tr.ui.b.BrushingState(); + bs1.selection = new EventSet([m1.sA]); + + // Change the model. + const m2 = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + + m.sA = m.t2.sliceGroup.pushSlice( + newSliceEx({title: 'a', start: 0, end: 5})); + }); + assert.isTrue(doesCauseChangeToFire( + brushingStateController, + function() { + brushingStateController.modelWillChange(); + timelineView.model = m2; + brushingStateController.modelDidChange(); + })); + assert.strictEqual( + brushingStateController.currentBrushingState.selection.length, 0); + }); + + function addChildDiv(element) { + const child = element.ownerDocument.createElement('div'); + Polymer.dom(element).appendChild(child); + return child; + } + + function addShadowChildDiv(element) { + const shadowRoot = element.createShadowRoot(); + return addChildDiv(shadowRoot); + } + + if (!tr.isHeadless) { + test('getControllerForElement_none', function() { + const element = document.createElement('div'); + + assert.isUndefined( + tr.c.BrushingStateController.getControllerForElement(element)); + }); + + test('getControllerForElement_self', function() { + const controller = new tr.c.BrushingStateController(undefined); + const element = document.createElement('div'); + element.brushingStateController = controller; + + assert.strictEqual( + tr.c.BrushingStateController.getControllerForElement(element), + controller); + }); + + test('getControllerForElement_ancestor', function() { + const controller = new tr.c.BrushingStateController(undefined); + const ancestor = document.createElement('div'); + ancestor.brushingStateController = controller; + + const element = addChildDiv(addChildDiv(addChildDiv(ancestor))); + assert.strictEqual( + tr.c.BrushingStateController.getControllerForElement(element), + controller); + }); + + test('getControllerForElement_host', function() { + const controller = new tr.c.BrushingStateController(undefined); + const host = document.createElement('div'); + host.brushingStateController = controller; + + const element = addShadowChildDiv(host); + assert.strictEqual( + tr.c.BrushingStateController.getControllerForElement(element), + controller); + }); + + test('getControllerForElement_hierarchy', function() { + const controller1 = new tr.c.BrushingStateController(undefined); + const root = document.createElement('div'); + root.brushingStateController = controller1; + + const controller2 = new tr.c.BrushingStateController(undefined); + const child = addChildDiv(root); + child.brushingStateController = controller2; + + const controller3 = new tr.c.BrushingStateController(undefined); + const shadowChild = addShadowChildDiv(child); + shadowChild.brushingStateController = controller3; + + const element = addChildDiv(addChildDiv(addShadowChildDiv( + addChildDiv(addChildDiv(addShadowChildDiv( + addChildDiv(shadowChild))))))); + assert.strictEqual( + tr.c.BrushingStateController.getControllerForElement(element), + controller3); + }); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_test.html b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_test.html new file mode 100644 index 00000000000..aa33b12d116 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_test.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/task.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/timeline_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + + const EventSet = tr.model.EventSet; + const SelectionState = tr.model.SelectionState; + + function newSimpleModel() { + return tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + + m.sA = m.t2.sliceGroup.pushSlice( + newSliceEx({title: 'a', start: 0, end: 5})); + m.sB = m.t2.sliceGroup.pushSlice( + newSliceEx({title: 'b', start: 10, end: 15})); + m.sC = m.t2.sliceGroup.pushSlice( + newSliceEx({title: 'c', start: 20, end: 20})); + }); + } + + test('brushingStateSimple', function() { + const m = newSimpleModel(); + + const bs = new tr.ui.b.BrushingState(); + bs.selection = new EventSet([m.sA]); + + bs.applyToEventSelectionStates(m); + assert.strictEqual(m.sA.selectionState, SelectionState.SELECTED); + bs.unapplyFromEventSelectionStates(); + assert.strictEqual(m.sA.selectionState, SelectionState.NONE); + }); + + + test('selectionAndAnalysisHover', function() { + const m = newSimpleModel(); + + const bs = new tr.ui.b.BrushingState(); + bs.selection = new EventSet([m.sA]); + bs.analysisLinkHoveredEvents = new EventSet([m.sA, m.sB]); + + bs.applyToEventSelectionStates(m); + assert.strictEqual(m.sA.selectionState, SelectionState.BRIGHTENED1); + assert.strictEqual(m.sB.selectionState, SelectionState.BRIGHTENED0); + bs.unapplyFromEventSelectionStates(); + assert.strictEqual(m.sA.selectionState, SelectionState.NONE); + }); + + test('brushingStateWithFindMatches', function() { + const m = newSimpleModel(); + + const bs = new tr.ui.b.BrushingState(); + bs.selection = new EventSet([m.sA]); + bs.findMatches = new EventSet([m.sA, m.sB]); + + bs.applyToEventSelectionStates(m); + assert.strictEqual(m.sA.selectionState, SelectionState.BRIGHTENED0); + assert.strictEqual(m.sB.selectionState, SelectionState.DIMMED1); + assert.strictEqual(m.sC.selectionState, SelectionState.DIMMED0); + bs.unapplyFromEventSelectionStates(); + assert.strictEqual(m.sA.selectionState, SelectionState.DIMMED0); + assert.strictEqual(m.sB.selectionState, SelectionState.DIMMED0); + assert.strictEqual(m.sC.selectionState, SelectionState.DIMMED0); + }); + + test('brushingTransfer', function() { + const m = newSimpleModel(); + + const bs = new tr.ui.b.BrushingState(); + bs.selection = new EventSet([m.sA]); + + const bs2 = bs.clone(); + + bs.applyToEventSelectionStates(m); + assert.strictEqual(m.sA.selectionState, SelectionState.SELECTED); + bs.transferModelOwnershipToClone(bs2); + assert.isFalse(bs.isAppliedToModel); + assert.isTrue(bs2.isAppliedToModel); + + bs2.unapplyFromEventSelectionStates(); + assert.strictEqual(m.sA.selectionState, SelectionState.NONE); + assert.strictEqual(m.sB.selectionState, SelectionState.NONE); + assert.strictEqual(m.sC.selectionState, SelectionState.NONE); + }); + + test('equality', function() { + const m = newSimpleModel(); + + const bs = new tr.ui.b.BrushingState(); + bs.selection = new EventSet([m.sA]); + bs.findMatches = new EventSet([m.sB]); + bs.applyToEventSelectionStates = new EventSet([m.sC]); + + // Clone equality, but with shared refs. + const bs2 = bs.clone(); + assert.isTrue(bs.equals(bs2)); + + // Same value, different refs. + bs2.selection = new EventSet([m.sA]); + assert.isTrue(bs.equals(bs2)); + + // Different actual values. + bs2.selection = new EventSet([m.sB]); + assert.isFalse(bs.equals(bs2)); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html new file mode 100644 index 00000000000..8e579d90921 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order> + +<link rel="stylesheet" href="/tracing/ui/extras/about_tracing/common.css"> +<link rel="import" href="/tracing/ui/extras/about_tracing/profiling_view.html"> +<link rel="import" href="/tracing/ui/extras/full_config.html"> +<script> +'use strict'; + +tr.exportTo('tr.ui.e.about_tracing', function() { + window.profilingView = undefined; // Made global for debugging purposes only. + + document.addEventListener('DOMContentLoaded', function() { + window.profilingView = new tr.ui.e.about_tracing.ProfilingView(); + profilingView.timelineView.globalMode = true; + Polymer.dom(document.body).appendChild(profilingView); + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css new file mode 100644 index 00000000000..3db21b67ffa --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css @@ -0,0 +1,25 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +html, +body { + height: 100%; +} + +body { + flex-direction: column; + display: flex; + margin: 0; + padding: 0; +} + +body > x-profiling-view { + flex: 1 1 auto; + min-height: 0; +} + +body > x-profiling-view > x-timeline-view:focus { + outline: 0 +} + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html new file mode 100644 index 00000000000..fa348a7b661 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html @@ -0,0 +1,99 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/base64.html"> +<script> + +'use strict'; + +/** + * A devtools protocol stream object. + * + * This reads a stream of data over the remote debugging connection. + */ +tr.exportTo('tr.ui.e.about_tracing', function() { + class DevtoolsStream { + constructor(connection, streamHandle) { + this.connection_ = connection; + this.streamHandle_ = streamHandle; + this.closed_ = false; + } + + async read() { + if (this.closed_) { + throw new Error('stream is closed'); + } + + const pendingRequests = []; + + const READ_REQUEST_BYTES = 32768; + const makeRequest = () => { + pendingRequests.push(this.connection_.req( + 'IO.read', + { + handle: this.streamHandle_, + size: READ_REQUEST_BYTES, + })); + }; + + const MAX_CONCURRENT_REQUESTS = 2; + for (let i = 0; i < MAX_CONCURRENT_REQUESTS; ++i) { + makeRequest(); + } + + const chunks = []; + let base64 = false; + while (true) { + const request = pendingRequests.shift(); + const response = await request; + + chunks.push(response.data); + if (response.base64Encoded) { + base64 = true; + } + if (response.eof) { + break; + } + + makeRequest(); + } + + if (base64) { + let totalSize = 0; + for (const chunk of chunks) { + totalSize += tr.b.Base64.getDecodedBufferLength(chunk); + } + const buffer = new ArrayBuffer(totalSize); + let offset = 0; + for (const chunk of chunks) { + offset += tr.b.Base64.DecodeToTypedArray( + chunk, + new DataView(buffer, offset)); + } + return buffer; + } + + return chunks.join(''); + } + + close() { + this.closed_ = true; + return this.connection_.req('IO.close', { handle: this.streamHandle_ }); + } + + async readAndClose() { + const data = await this.read(); + this.close(); + return data; + } + } + + return { + DevtoolsStream, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html new file mode 100644 index 00000000000..791f5e77705 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html @@ -0,0 +1,115 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> +<script> + +'use strict'; + +/** + * Contains connection code that inspector's embedding framework calls on + * tracing, and that tracing can use to talk to inspector. + */ +tr.exportTo('tr.ui.e.about_tracing', function() { + class InspectorConnection { + constructor(windowGlobal) { + if (!windowGlobal.DevToolsHost) { + throw new Error('Requires window.DevToolsHost'); + } + this.devToolsHost_ = windowGlobal.DevToolsHost; + this.installDevToolsAPI_(windowGlobal); + + this.nextRequestId_ = 1; + this.pendingRequestResolversId_ = {}; + + this.notificationListenersByMethodName_ = {}; + } + + req(method, params) { + const id = this.nextRequestId_++; + const msg = JSON.stringify({ + id, + method, + params + }); + const devtoolsMessageStr = JSON.stringify( + {id, 'method': 'dispatchProtocolMessage', 'params': [msg]}); + this.devToolsHost_.sendMessageToEmbedder(devtoolsMessageStr); + + return new Promise(function(resolve, reject) { + this.pendingRequestResolversId_[id] = { + resolve, + reject + }; + }.bind(this)); + } + + setNotificationListener(method, listener) { + this.notificationListenersByMethodName_[method] = listener; + } + + dispatchMessage_(payload) { + const isStringPayload = typeof payload === 'string'; + // Special handling for Tracing.dataCollected because it is high + // bandwidth. + const isDataCollectedMessage = isStringPayload ? + payload.includes('"method": "Tracing.dataCollected"') : + payload.method === 'Tracing.dataCollected'; + if (isDataCollectedMessage) { + const listener = this.notificationListenersByMethodName_[ + 'Tracing.dataCollected']; + if (listener) { + // FIXME(loislo): trace viewer should be able to process + // raw message object because string based version a few times + // slower on the browser side. + // see https://codereview.chromium.org/784513002. + listener(isStringPayload ? payload : JSON.stringify(payload)); + return; + } + } + + const message = isStringPayload ? JSON.parse(payload) : payload; + if (message.id) { + const resolver = this.pendingRequestResolversId_[message.id]; + if (resolver === undefined) { + return; + } + if (message.error) { + resolver.reject(message.error); + return; + } + resolver.resolve(message.result); + return; + } + + if (message.method) { + const listener = this.notificationListenersByMethodName_[ + message.method]; + if (listener === undefined) return; + listener(message.params); + return; + } + } + + installDevToolsAPI_(windowGlobal) { + // Interface used by inspector when it hands data to us from the backend. + windowGlobal.DevToolsAPI = { + setToolbarColors() { }, + addExtensions() { }, + setInspectedPageId() { }, + dispatchMessage: this.dispatchMessage_.bind(this), + }; + + // Temporary until inspector backend switches to DevToolsAPI. + windowGlobal.InspectorFrontendAPI = windowGlobal.DevToolsAPI; + } + } + + return { + InspectorConnection, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html new file mode 100644 index 00000000000..ac5afabae12 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html @@ -0,0 +1,216 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/extras/about_tracing/devtools_stream.html"> +<link rel="import" href="/tracing/ui/extras/about_tracing/inspector_connection.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/tracing_controller_client.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.about_tracing', function() { + function createResolvedPromise(data) { + const promise = new Promise(function(resolve, reject) { + if (data) { + resolve(data); + } else { + resolve(); + } + }); + return promise; + } + + function appendTraceChunksTo(chunks, messageString) { + if (typeof messageString !== 'string') { + throw new Error('Invalid data'); + } + const re = /"params":\s*\{\s*"value":\s*\[([^]+)\]\s*\}\s*\}/; + const m = re.exec(messageString); + if (!m) { + throw new Error('Malformed response'); + } + + if (chunks.length > 1) { + chunks.push(','); + } + chunks.push(m[1]); + } + + /** + * Controls tracing using the inspector's FrontendAgentHost APIs. + */ + class InspectorTracingControllerClient extends + tr.ui.e.about_tracing.TracingControllerClient { + constructor(connection) { + super(); + this.recording_ = false; + this.bufferUsage_ = 0; + this.conn_ = connection; + this.currentTraceTextChunks_ = undefined; + } + + beginMonitoring(monitoringOptions) { + throw new Error('Not implemented'); + } + + endMonitoring() { + throw new Error('Not implemented'); + } + + captureMonitoring() { + throw new Error('Not implemented'); + } + + getMonitoringStatus() { + return createResolvedPromise({ + isMonitoring: false, + categoryFilter: '', + useSystemTracing: false, + useContinuousTracing: false, + useSampling: false + }); + } + + getCategories() { + const res = this.conn_.req('Tracing.getCategories', {}); + return res.then(function(result) { + return result.categories; + }, function(err) { + return []; + }); + } + + beginRecording(recordingOptions) { + if (this.recording_) { + throw new Error('Already recording'); + } + this.recording_ = 'starting'; + + // The devtools and tracing endpoints have slightly different parameter + // configurations. Noteably, recordMode has different spelling + // requirements. + function RewriteRecordMode(recordMode) { + if (recordMode === 'record-until-full') { + return 'recordUntilFull'; + } + if (recordMode === 'record-continuously') { + return 'recordContinuously'; + } + if (recordMode === 'record-as-much-as-possible') { + return 'recordAsMuchAsPossible'; + } + return 'unsupported record mode'; + } + + const traceConfigStr = { + includedCategories: recordingOptions.included_categories, + excludedCategories: recordingOptions.excluded_categories, + recordMode: RewriteRecordMode(recordingOptions.record_mode), + enableSystrace: recordingOptions.enable_systrace + }; + if ('memory_dump_config' in recordingOptions) { + traceConfigStr.memoryDumpConfig = recordingOptions.memory_dump_config; + } + let res = this.conn_.req( + 'Tracing.start', + { + traceConfig: traceConfigStr, + transferMode: 'ReturnAsStream', + streamCompression: 'gzip', + bufferUsageReportingInterval: 1000 + }); + res = res.then( + function ok() { + this.conn_.setNotificationListener( + 'Tracing.bufferUsage', + this.onBufferUsageUpdateFromInspector_.bind(this)); + this.recording_ = true; + }.bind(this), + function error() { + this.recording_ = false; + }.bind(this)); + return res; + } + + onBufferUsageUpdateFromInspector_(params) { + this.bufferUsage_ = params.value || params.percentFull; + } + + beginGetBufferPercentFull() { + return tr.b.timeout(100).then(() => this.bufferUsage_); + } + + onDataCollected_(messageString) { + appendTraceChunksTo(this.currentTraceTextChunks_, messageString); + } + + async endRecording() { + if (this.recording_ === false) { + return createResolvedPromise(); + } + + if (this.recording_ !== true) { + throw new Error('Cannot end'); + } + + this.currentTraceTextChunks_ = ['[']; + const clearListeners = () => { + this.conn_.setNotificationListener( + 'Tracing.bufferUsage', undefined); + this.conn_.setNotificationListener( + 'Tracing.tracingComplete', undefined); + this.conn_.setNotificationListener( + 'Tracing.dataCollected', undefined); + }; + + try { + this.conn_.setNotificationListener( + 'Tracing.dataCollected', this.onDataCollected_.bind(this)); + + const tracingComplete = new Promise((resolve, reject) => { + this.conn_.setNotificationListener( + 'Tracing.tracingComplete', resolve); + }); + + this.recording_ = 'stopping'; + await this.conn_.req('Tracing.end', {}); + const params = await tracingComplete; + + this.traceName_ = 'trace.json'; + if ('stream' in params) { + const stream = new tr.ui.e.about_tracing.DevtoolsStream( + this.conn_, params.stream); + const streamCompression = params.streamCompression || 'none'; + if (streamCompression === 'gzip') { + this.traceName_ = 'trace.json.gz'; + } + + return await stream.readAndClose(); + } + + this.currentTraceTextChunks_.push(']'); + const traceText = this.currentTraceTextChunks_.join(''); + this.currentTraceTextChunks_ = undefined; + return traceText; + } finally { + clearListeners(); + this.recording_ = false; + } + } + + defaultTraceName() { + return this.traceName_; + } + } + + return { + InspectorTracingControllerClient, + appendTraceChunksTo, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html new file mode 100644 index 00000000000..4a6585ac9e8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html @@ -0,0 +1,396 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" + href="/tracing/ui/extras/about_tracing/inspector_connection.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html"> + +<script> +'use strict'; + +function makeController() { + const controller = + new tr.ui.e.about_tracing.InspectorTracingControllerClient(); + controller.conn_ = new (function() { + this.req = function(method, params) { + const msg = JSON.stringify({ + id: 1, + method, + params + }); + return new (function() { + this.msg = msg; + this.then = function(m1, m2) { + return this; + }; + })(); + }; + this.setNotificationListener = function(method, listener) { + }; + })(); + return controller; +} + +tr.b.unittest.testSuite(function() { + test('beginRecording_sendCategoriesAndOptions', function() { + const controller = makeController(); + + const recordingOptions = { + included_categories: ['a', 'b', 'c'], + excluded_categories: ['e'], + enable_systrace: false, + record_mode: 'record-until-full', + }; + + const result = JSON.parse(controller.beginRecording(recordingOptions).msg); + assert.deepEqual( + result.params.traceConfig.includedCategories, ['a', 'b', 'c']); + assert.deepEqual( + result.params.traceConfig.excludedCategories, ['e']); + assert.strictEqual( + result.params.traceConfig.recordMode, 'recordUntilFull'); + assert.isFalse( + result.params.traceConfig.enableSystrace); + assert.isFalse('memoryDumpConfig' in result.params.traceConfig); + }); + + test('beginRecording_sendCategoriesAndOptionsWithMemoryInfra', function() { + const controller = makeController(); + + const memoryConfig = { triggers: [] }; + memoryConfig.triggers.push( + {'mode': 'detailed', 'periodic_interval_ms': 10000}); + const recordingOptions = { + included_categories: ['c', 'disabled-by-default-memory-infra', 'a'], + excluded_categories: ['e'], + enable_systrace: false, + record_mode: 'test-mode', + memory_dump_config: memoryConfig, + }; + + const result = JSON.parse(controller.beginRecording(recordingOptions).msg); + assert.isTrue( + result.params.traceConfig.memoryDumpConfig.triggers.length === 1); + assert.strictEqual(result.params.traceConfig.memoryDumpConfig. + triggers[0].mode, 'detailed'); + assert.strictEqual(result.params.traceConfig.memoryDumpConfig. + triggers[0].periodic_interval_ms, 10000); + }); + + test('oldFormat', function() { + const chunks = []; + tr.ui.e.about_tracing.appendTraceChunksTo(chunks, '"{ "method": "Tracing.dataCollected", "params": { "value": [ {"cat":"__metadata","pid":28871,"tid":0,"ts":0,"ph":"M","name":"num_cpus","args":{"number":4}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_sort_index","args":{"sort_index":-5}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_name","args":{"name":"Renderer"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_labels","args":{"labels":"JS Bin"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_sort_index","args":{"sort_index":-1}},{"cat":"__metadata","pid":28871,"tid":28917,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Compositor"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Chrome_ChildIOThread"}},{"cat":"__metadata","pid":28871,"tid":28919,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CompositorRasterWorker1/28919"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CrRendererMain"}},{"cat":"ipc,toplevel","pid":28871,"tid":28911,"ts":22000084746,"ph":"X","name":"ChannelReader::DispatchInputData","args":{"class":64,"line":25},"tdur":0,"tts":1853064},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"overhead","args":{"average_overhead":0.015}} ] } }"'); // @suppress longLineCheck + assert.strictEqual(chunks.length, 1); + JSON.parse('[' + chunks.join('') + ']'); + }); + + test('newFormat', function() { + const chunks = []; + tr.ui.e.about_tracing.appendTraceChunksTo(chunks, '"{ "method": "Tracing.dataCollected", "params": { "value": [{"cat":"__metadata","pid":28871,"tid":0,"ts":0,"ph":"M","name":"num_cpus","args":{"number":4}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_sort_index","args":{"sort_index":-5}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_name","args":{"name":"Renderer"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_labels","args":{"labels":"JS Bin"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_sort_index","args":{"sort_index":-1}},{"cat":"__metadata","pid":28871,"tid":28917,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Compositor"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Chrome_ChildIOThread"}},{"cat":"__metadata","pid":28871,"tid":28919,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CompositorRasterWorker1/28919"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CrRendererMain"}},{"cat":"ipc,toplevel","pid":28871,"tid":28911,"ts":22000084746,"ph":"X","name":"ChannelReader::DispatchInputData","args":{"class":64,"line":25},"tdur":0,"tts":1853064},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"overhead","args":{"average_overhead":0.015}}] } }"'); // @suppress longLineCheck + assert.strictEqual(chunks.length, 1); + JSON.parse('[' + chunks.join('') + ']'); + }); + + test('stringAndObjectPayload', function() { + const connection = + new tr.ui.e.about_tracing.InspectorConnection({DevToolsHost: {}}); + connection.setNotificationListener('Tracing.dataCollected', + function(message) { + assert.typeOf(message, 'string'); + JSON.parse(message); + } + ); + connection.dispatchMessage_('{ "method": "Tracing.dataCollected", "params": { "value": [] } }'); // @suppress longLineCheck + connection.dispatchMessage_({'method': 'Tracing.dataCollected', 'params': {'value': [] } }); // @suppress longLineCheck + }); + + // Makes a fake version of DevToolsHost, which is the object injected + // by the chrome inspector to allow tracing a remote instance of chrome. + // + // The fake host doesn't do much by itself - you have to install + // callbacks for incoming messages via handleMessage(). + function makeFakeDevToolsHost() { + return new (function() { + this.pendingMethods_ = []; + this.messageHandlers_ = []; + + // Sends a message to DevTools host. This is used by + // InspectorTracingControllerClient to communicate with the remote + // debugging tracing backend. + this.sendMessageToEmbedder = function(devtoolsMessageStr) { + this.pendingMethods_.push(JSON.parse(devtoolsMessageStr)); + this.tryMessageHandlers_(); + }; + + // Runs remote debugging message handlers. Handlers are installed + // by test code via handleMessage(). + this.tryMessageHandlers_ = function() { + while (this.pendingMethods_.length !== 0) { + const message = this.pendingMethods_[0]; + const params = JSON.parse(message.params); + let handled = false; + const handlersToRemove = []; + + // Try to find a handler for this method. + for (const handler of this.messageHandlers_) { + if (handler(params, () => handlersToRemove.push(handler))) { + handled = true; + break; + } + } + + // Remove any handlers that requested removal. + this.messageHandlers_ = this.messageHandlers_.filter( + (handler) => !handlersToRemove.includes(handler)); + + // Remove any handled messages. + if (handled) { + this.pendingMethods_.shift(); + } else { + return; // Methods must be handled in order. + } + } + }; + + // Installs a message handler that will be invoked for each + // incoming message from InspectorTracingControllerClient. + // + // handleMessage((message, removeSelf) => { + // // Try to handle |message|. + // // Call |removeSelf| to remove this handler for future messages. + // // Return whether |message| was handled. Otherwise other handlers + // // will be run until one of them succeeds. + // } + this.handleMessage = function(handler) { + this.messageHandlers_.push(handler); + this.tryMessageHandlers_(); + }; + + // Installs a message handler that will handle the first call to the named + // method. Returns a promise for the parameters passed to the method. + this.handleMethod = function(method) { + const result = new Promise((resolve, reject) => { + this.handleMessage( + (requestParams, removeHandler) => { + if (requestParams.method === method) { + removeHandler(); + resolve(requestParams); + return true; + } + return false; + }); + }); + return result; + }; + + // Sends a response to a remote debugging method call (i.e., + // "return") to InspectorTracingControllerClient. + this.respondToMethod = function(id, params) { + this.devToolsAPI_.dispatchMessage(JSON.stringify({ + id, + result: params, + })); + }; + + // Sets the object used to send messages back to + // InspectorTracingControllerClient. + this.setDevToolsAPI = function(api) { + this.devToolsAPI_ = api; + }; + + // Sends a notification to InspectorTracingControllerClient. + this.sendNotification = function(method, params) { + this.devToolsAPI_.dispatchMessage(JSON.stringify({ method, params })); + }; + })(); + } + + test('shouldUseLegacyTraceFormatIfNoStreamId', async function() { + const fakeDevToolsHost = makeFakeDevToolsHost(); + const fakeWindow = { + DevToolsHost: fakeDevToolsHost, + }; + const controller = + new tr.ui.e.about_tracing.InspectorTracingControllerClient( + new tr.ui.e.about_tracing.InspectorConnection(fakeWindow)); + fakeDevToolsHost.setDevToolsAPI(fakeWindow.DevToolsAPI); + + const runHost = (async() => { + const startParams = await fakeDevToolsHost.handleMethod('Tracing.start'); + fakeDevToolsHost.respondToMethod(startParams.id, {}); + const endParams = await fakeDevToolsHost.handleMethod('Tracing.end'); + fakeDevToolsHost.respondToMethod(endParams.id, {}); + fakeDevToolsHost.sendNotification('Tracing.tracingComplete', {}); + })(); + + await controller.beginRecording({}); + const traceData = await controller.endRecording(); + await runHost; + + assert.strictEqual('[]', traceData); + }); + + test('shouldReassembleTextDataChunks', async function() { + const fakeDevToolsHost = makeFakeDevToolsHost(); + const fakeWindow = { + DevToolsHost: fakeDevToolsHost, + }; + const controller = + new tr.ui.e.about_tracing.InspectorTracingControllerClient( + new tr.ui.e.about_tracing.InspectorConnection(fakeWindow)); + fakeDevToolsHost.setDevToolsAPI(fakeWindow.DevToolsAPI); + + const STREAM_HANDLE = 7; + + const streamChunks = [ + '[', + ']', + '\n', + ]; + + let streamClosed = false; + + const handleIoRead = (index, params) => { + if (params.params.handle !== STREAM_HANDLE) { + throw new Error('Invalid stream handle'); + } + if (streamClosed) { + throw new Error('stream is closed'); + } + let data = ''; + if (index < streamChunks.length) { + data = streamChunks[index]; + } + const eof = (index >= streamChunks.length - 1); + fakeDevToolsHost.respondToMethod(params.id, { + eof, + base64Encoded: false, + data, + }); + const nextIndex = eof ? streamChunks.length : index + 1; + return (async() => + handleIoRead(nextIndex, await fakeDevToolsHost.handleMethod('IO.read')) + )(); + }; + + const runHost = (async() => { + const startParams = await fakeDevToolsHost.handleMethod('Tracing.start'); + fakeDevToolsHost.respondToMethod(startParams.id, {}); + const endParams = await fakeDevToolsHost.handleMethod('Tracing.end'); + fakeDevToolsHost.respondToMethod(endParams.id, {}); + fakeDevToolsHost.sendNotification('Tracing.tracingComplete', { + 'stream': STREAM_HANDLE, + }); + + const closePromise = (async() => { + const closeParams = await fakeDevToolsHost.handleMethod('IO.close'); + assert.strictEqual(closeParams.params.handle, STREAM_HANDLE); + streamClosed = true; + })(); + + const readPromise = (async() => + handleIoRead(0, await fakeDevToolsHost.handleMethod('IO.read')) + )(); + + await Promise.race([closePromise, readPromise]); + await closePromise; + })(); + + await controller.beginRecording({}); + const traceData = await controller.endRecording(); + await runHost; + + assert.strictEqual(traceData, '[]\n'); + }); + + test('shouldReassembleBase64TraceDataChunks', async function() { + const fakeDevToolsHost = makeFakeDevToolsHost(); + const fakeWindow = { + DevToolsHost: fakeDevToolsHost, + }; + const controller = + new tr.ui.e.about_tracing.InspectorTracingControllerClient( + new tr.ui.e.about_tracing.InspectorConnection(fakeWindow)); + fakeDevToolsHost.setDevToolsAPI(fakeWindow.DevToolsAPI); + + const STREAM_HANDLE = 7; + + // This is the empty trace ('[]') gzip compressed and chunked to make + // sure reassembling base64 strings works properly. + const streamChunks = [ + 'Hw==', + 'iwg=', + 'ALg4', + 'L1oAA4uOBQApu0wNAgAAAA==', + ]; + + let streamClosed = false; + + const handleIoRead = (index, params) => { + if (params.params.handle !== STREAM_HANDLE) { + throw new Error('Invalid stream handle'); + } + if (streamClosed) { + throw new Error('stream is closed'); + } + let data = ''; + if (index < streamChunks.length) { + data = streamChunks[index]; + } + const eof = (index >= streamChunks.length - 1); + fakeDevToolsHost.respondToMethod(params.id, { + eof, + base64Encoded: true, + data, + }); + const nextIndex = eof ? streamChunks.length : index + 1; + return (async() => { + handleIoRead(nextIndex, await fakeDevToolsHost.handleMethod('IO.read')); + })(); + }; + + const runHost = (async() => { + const startParams = await fakeDevToolsHost.handleMethod('Tracing.start'); + fakeDevToolsHost.respondToMethod(startParams.id, {}); + const endParams = await fakeDevToolsHost.handleMethod('Tracing.end'); + fakeDevToolsHost.respondToMethod(endParams.id, {}); + fakeDevToolsHost.sendNotification('Tracing.tracingComplete', { + 'stream': STREAM_HANDLE, + 'streamCompression': 'gzip' + }); + const closePromise = (async() => { + const closeParams = await fakeDevToolsHost.handleMethod('IO.close'); + assert.strictEqual(closeParams.params.handle, STREAM_HANDLE); + streamClosed = true; + })(); + + const readPromise = (async() => { + handleIoRead(0, await fakeDevToolsHost.handleMethod('IO.read')); + })(); + + await Promise.race([closePromise, readPromise]); + await closePromise; + })(); + + await controller.beginRecording({}); + const traceData = await controller.endRecording(); + await runHost; + + const dataArray = new Uint8Array(traceData); + const expectedArray = new Uint8Array([ + 0x1f, 0x8b, 0x8, 0x0, 0xb8, 0x38, 0x2f, 0x5a, 0x0, 0x3, 0x8b, 0x8e, + 0x5, 0x0, 0x29, 0xbb, 0x4c, 0xd, 0x2, 0x0, 0x0, 0x0]); + + assert.strictEqual(dataArray.length, expectedArray.length); + + for (let i = 0; i < dataArray.length; ++i) { + assert.strictEqual(dataArray[i], expectedArray[i]); + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html new file mode 100644 index 00000000000..cfefdc05cc7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" + href="/tracing/ui/extras/about_tracing/tracing_controller_client.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.about_tracing', function() { + function MockTracingControllerClient() { + this.requests = []; + this.nextRequestIndex = 0; + this.allowLooping = false; + } + + MockTracingControllerClient.prototype = { + __proto__: tr.ui.e.about_tracing.TracingControllerClient.prototype, + + expectRequest(method, generateResponse) { + let generateResponseCb; + if (typeof generateResponse === 'function') { + generateResponseCb = generateResponse; + } else { + generateResponseCb = function() { + return generateResponse; + }; + } + + this.requests.push({ + method, + generateResponseCb}); + }, + + _request(method, args) { + return new Promise(function(resolve) { + const requestIndex = this.nextRequestIndex; + if (requestIndex >= this.requests.length) { + throw new Error('Unhandled request'); + } + if (!this.allowLooping) { + this.nextRequestIndex++; + } else { + this.nextRequestIndex = (this.nextRequestIndex + 1) % + this.requests.length; + } + + const req = this.requests[requestIndex]; + assert.strictEqual(method, req.method); + const resp = req.generateResponseCb(args); + resolve(resp); + }.bind(this)); + }, + + assertAllRequestsHandled() { + if (this.allowLooping) { + throw new Error('Incompatible with allowLooping'); + } + assert.strictEqual(this.requests.length, this.nextRequestIndex); + }, + + getCategories() { + return this._request('getCategories'); + }, + + beginRecording(recordingOptions) { + return this._request('beginRecording', recordingOptions); + }, + + beginGetBufferPercentFull() { + return this._request('beginGetBufferPercentFull'); + }, + + endRecording() { + return this._request('endRecording'); + } + }; + + return { + MockTracingControllerClient, + }; +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html new file mode 100644 index 00000000000..77c0e80af12 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html @@ -0,0 +1,372 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base64.html"> +<link rel="import" href="/tracing/importer/import.html"> +<link rel="import" href="/tracing/ui/base/file.html"> +<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> +<link rel="import" href="/tracing/ui/base/info_bar_group.html"> +<link rel="import" href="/tracing/ui/base/overlay.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/record_controller.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html"> +<link rel="import" href="/tracing/ui/timeline_view.html"> + +<style> +x-profiling-view { + flex-direction: column; + display: flex; + padding: 0; +} + +x-profiling-view .controls #save-button { + margin-left: 64px !important; +} + +x-profiling-view > tr-ui-timeline-view { + flex: 1 1 auto; + min-height: 0; +} + +.report-id-message { + -webkit-user-select: text; +} + +x-timeline-view-buttons { + display: flex; + align-items: center; +} +</style> + +<template id="profiling-view-template"> + <tr-ui-b-info-bar-group></tr-ui-b-info-bar-group> + <x-timeline-view-buttons> + <button id="record-button">Record</button> + <button id="save-button">Save</button> + <button id="load-button">Load</button> + </x-timeline-view-buttons> + <tr-ui-timeline-view> + <track-view-container id='track_view_container'></track-view-container> + </tr-ui-timeline-view> +</template> + +<script> +'use strict'; + +/** + * @fileoverview ProfilingView glues the View control to + * TracingController. + */ +tr.exportTo('tr.ui.e.about_tracing', function() { + /** + * ProfilingView + * @constructor + * @extends {HTMLDivElement} + */ + const ProfilingView = tr.ui.b.define('x-profiling-view'); + const THIS_DOC = document.currentScript.ownerDocument; + + ProfilingView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate(tracingControllerClient) { + Polymer.dom(this).appendChild( + tr.ui.b.instantiateTemplate('#profiling-view-template', THIS_DOC)); + + this.timelineView_ = + Polymer.dom(this).querySelector('tr-ui-timeline-view'); + this.infoBarGroup_ = + Polymer.dom(this).querySelector('tr-ui-b-info-bar-group'); + + // Detach the buttons. We will reattach them to the timeline view. + // TODO(nduca): Make timeline-view have a content select="x-buttons" + // that pulls in any buttons. + this.recordButton_ = Polymer.dom(this).querySelector('#record-button'); + this.loadButton_ = Polymer.dom(this).querySelector('#load-button'); + this.saveButton_ = Polymer.dom(this).querySelector('#save-button'); + + const buttons = Polymer.dom(this).querySelector( + 'x-timeline-view-buttons'); + Polymer.dom(buttons.parentElement).removeChild(buttons); + Polymer.dom(this.timelineView_.leftControls).appendChild(buttons); + this.initButtons_(); + + this.timelineView_.hotkeyController.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keypress', + keyCode: 'r'.charCodeAt(0), + callback(e) { + this.beginRecording(); + event.stopPropagation(); + }, + thisArg: this + })); + + this.initDragAndDrop_(); + + if (tracingControllerClient) { + this.tracingControllerClient_ = tracingControllerClient; + } else if (window.DevToolsHost !== undefined) { + this.tracingControllerClient_ = + new tr.ui.e.about_tracing.InspectorTracingControllerClient( + new tr.ui.e.about_tracing.InspectorConnection(window)); + } else { + this.tracingControllerClient_ = + new tr.ui.e.about_tracing.XhrBasedTracingControllerClient(); + } + + this.isRecording_ = false; + this.activeTrace_ = undefined; + + this.updateTracingControllerSpecificState_(); + }, + + // Detach all document event listeners. Without this the tests can get + // confused as the element may still be listening when the next test runs. + detach_() { + this.detachDragAndDrop_(); + }, + + get isRecording() { + return this.isRecording_; + }, + + set tracingControllerClient(tracingControllerClient) { + this.tracingControllerClient_ = tracingControllerClient; + this.updateTracingControllerSpecificState_(); + }, + + updateTracingControllerSpecificState_() { + const isInspector = this.tracingControllerClient_ instanceof + tr.ui.e.about_tracing.InspectorTracingControllerClient; + + if (isInspector) { + this.infoBarGroup_.addMessage( + 'This about:tracing is connected to a remote device...', + [{buttonText: 'Wow!', onClick() {}}]); + } + }, + + beginRecording() { + if (this.isRecording_) { + throw new Error('Already recording'); + } + this.isRecording_ = true; + const resultPromise = tr.ui.e.about_tracing.beginRecording( + this.tracingControllerClient_); + resultPromise.then( + function(data) { + this.isRecording_ = false; + const traceName = tr.ui.e.about_tracing.defaultTraceName( + this.tracingControllerClient_); + this.setActiveTrace(traceName, data, false); + }.bind(this), + function(err) { + this.isRecording_ = false; + if (err instanceof tr.ui.e.about_tracing.UserCancelledError) { + return; + } + tr.ui.b.Overlay.showError('Error while recording', err); + }.bind(this)); + return resultPromise; + }, + + get timelineView() { + return this.timelineView_; + }, + + /////////////////////////////////////////////////////////////////////////// + + clearActiveTrace() { + this.saveButton_.disabled = true; + this.activeTrace_ = undefined; + }, + + setActiveTrace(filename, data) { + this.activeTrace_ = { + filename, + data + }; + + this.infoBarGroup_.clearMessages(); + this.updateTracingControllerSpecificState_(); + this.saveButton_.disabled = false; + this.timelineView_.viewTitle = filename; + + const m = new tr.Model(); + const i = new tr.importer.Import(m); + const p = i.importTracesWithProgressDialog([data]); + p.then( + function() { + this.timelineView_.model = m; + this.timelineView_.updateDocumentFavicon(); + }.bind(this), + function(err) { + tr.ui.b.Overlay.showError('While importing: ', err); + }.bind(this)); + }, + + /////////////////////////////////////////////////////////////////////////// + + initButtons_() { + this.recordButton_.addEventListener( + 'click', function(event) { + event.stopPropagation(); + this.beginRecording(); + }.bind(this)); + + this.loadButton_.addEventListener( + 'click', function(event) { + event.stopPropagation(); + this.onLoadClicked_(); + }.bind(this)); + + this.saveButton_.addEventListener('click', + this.onSaveClicked_.bind(this)); + this.saveButton_.disabled = true; + }, + + requestFilename_() { + // unsafe filename patterns: + const illegalRe = /[\/\?<>\\:\*\|":]/g; + const controlRe = /[\x00-\x1f\x80-\x9f]/g; + const reservedRe = /^\.+$/; + + const defaultName = this.activeTrace_.filename; + let fileExtension = '.json'; + let fileRegex = /\.json$/; + if (/[.]gz$/.test(defaultName)) { + fileExtension += '.gz'; + fileRegex = /\.json\.gz$/; + } else if (/[.]zip$/.test(defaultName)) { + fileExtension = '.zip'; + fileRegex = /\.zip$/; + } + + const custom = prompt('Filename? (' + fileExtension + + ' appended) Or leave blank:'); + if (custom === null) { + return undefined; + } + + let name; + if (custom) { + name = ' ' + custom; + } else { + const date = new Date(); + const dateText = ' ' + date.toDateString() + + ' ' + date.toLocaleTimeString(); + name = dateText; + } + + const filename = defaultName.replace(fileRegex, name) + fileExtension; + + return filename + .replace(illegalRe, '.') + .replace(controlRe, '\u2022') + .replace(reservedRe, '') + .replace(/\s+/g, '_'); + }, + + onSaveClicked_() { + // Create a blob URL from the binary array. + const blob = new Blob([this.activeTrace_.data], + {type: 'application/octet-binary'}); + const blobUrl = window.webkitURL.createObjectURL(blob); + + // Create a link and click on it. BEST API EVAR! + const link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); + link.href = blobUrl; + const filename = this.requestFilename_(); + if (filename) { + link.download = filename; + link.click(); + } + }, + + onLoadClicked_() { + const inputElement = document.createElement('input'); + inputElement.type = 'file'; + inputElement.multiple = false; + + let changeFired = false; + inputElement.addEventListener( + 'change', + function(e) { + if (changeFired) return; + changeFired = true; + + const file = inputElement.files[0]; + tr.ui.b.readFile(file).then( + function(data) { + this.setActiveTrace(file.name, data); + }.bind(this), + function(err) { + tr.ui.b.Overlay.showError('Error while loading file: ' + err); + }); + }.bind(this), false); + inputElement.click(); + }, + + /////////////////////////////////////////////////////////////////////////// + + initDragAndDrop_() { + this.dropHandler_ = this.dropHandler_.bind(this); + this.ignoreDragEvent_ = this.ignoreDragEvent_.bind(this); + document.addEventListener('dragstart', this.ignoreDragEvent_, false); + document.addEventListener('dragend', this.ignoreDragEvent_, false); + document.addEventListener('dragenter', this.ignoreDragEvent_, false); + document.addEventListener('dragleave', this.ignoreDragEvent_, false); + document.addEventListener('dragover', this.ignoreDragEvent_, false); + document.addEventListener('drop', this.dropHandler_, false); + }, + + detachDragAndDrop_() { + document.removeEventListener('dragstart', this.ignoreDragEvent_); + document.removeEventListener('dragend', this.ignoreDragEvent_); + document.removeEventListener('dragenter', this.ignoreDragEvent_); + document.removeEventListener('dragleave', this.ignoreDragEvent_); + document.removeEventListener('dragover', this.ignoreDragEvent_); + document.removeEventListener('drop', this.dropHandler_); + }, + + ignoreDragEvent_(e) { + e.preventDefault(); + return false; + }, + + dropHandler_(e) { + if (this.isAnyDialogUp_) return; + + e.stopPropagation(); + e.preventDefault(); + + const files = e.dataTransfer.files; + if (files.length !== 1) { + tr.ui.b.Overlay.showError('1 file supported at a time.'); + return; + } + + tr.ui.b.readFile(files[0]).then( + function(data) { + this.setActiveTrace(files[0].name, data); + }.bind(this), + function(err) { + tr.ui.b.Overlay.showError('Error while loading file: ' + err); + }); + return false; + } + }; + + return { + ProfilingView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html new file mode 100644 index 00000000000..f52c491207f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/base64.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html"> +<link rel="import" href="/tracing/ui/extras/about_tracing/profiling_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Base64 = tr.b.Base64; + const testData = [ + {name: 'a', args: {}, pid: 52, ts: 15000, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'a', args: {}, pid: 52, ts: 19000, cat: 'foo', tid: 53, ph: 'E'}, + {name: 'b', args: {}, pid: 52, ts: 32000, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'b', args: {}, pid: 52, ts: 54000, cat: 'foo', tid: 53, ph: 'E'} + ]; + + const monitoringOptions = { + isMonitoring: false, + categoryFilter: '*', + useSystemTracing: false, + useContinuousTracing: false, + useSampling: false + }; + + const ProfilingView = tr.ui.e.about_tracing.ProfilingView; + + test('recording', function() { + const mock = new tr.ui.e.about_tracing.MockTracingControllerClient(); + mock.allowLooping = true; + mock.expectRequest('endRecording', function() { + return ''; + }); + mock.expectRequest('getCategories', function() { + return ['a', 'b', 'c']; + }); + mock.expectRequest('beginRecording', function(data) { + return ''; + }); + mock.expectRequest('endRecording', function(data) { + return JSON.stringify(testData); + }); + + const view = new ProfilingView(mock); + view.style.height = '400px'; + view.style.border = '1px solid black'; + this.addHTMLOutput(view); + + const recordingPromise = view.beginRecording(); + + let didAbort = false; + + tr.b.timeout(60).then(() => { + if (didAbort) return; + recordingPromise.selectionDlg.clickRecordButton(); + }).then(() => tr.b.timeout(60)).then(() => { + recordingPromise.progressDlg.clickStopButton(); + }); + + return recordingPromise.then(null, err => { + didAbort = true; + assert.fail(err); + }); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html new file mode 100644 index 00000000000..a9b42b589d8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html @@ -0,0 +1,187 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/extras/about_tracing/record_selection_dialog.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.about_tracing', function() { + function beginRecording(tracingControllerClient) { + let finalPromiseResolver; + const finalPromise = new Promise(function(resolve, reject) { + finalPromiseResolver = { + resolve, + reject + }; + }); + finalPromise.selectionDlg = undefined; + finalPromise.progressDlg = undefined; + + function beginRecordingError(err) { + finalPromiseResolver.reject(err); + } + + // Step 0: End recording. This is necessary when the user reloads the + // about:tracing page when we are recording. Window.onbeforeunload is not + // reliable to end recording on reload. + endRecording(tracingControllerClient).then( + getCategories, + getCategories); // Ignore error. + + // But just in case, bind onbeforeunload anyway. + window.onbeforeunload = function(e) { + endRecording(tracingControllerClient); + }; + + // Step 1: Get categories. + function getCategories() { + const p = tracingControllerClient.getCategories().then( + showTracingDialog, + beginRecordingError); + p.catch(function(err) { + beginRecordingError(err); + }); + } + + // Step 2: Show tracing dialog. + let selectionDlg; + function showTracingDialog(categories) { + selectionDlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + selectionDlg.categories = categories; + selectionDlg.settings_key = + 'tr.ui.e.about_tracing.record_selection_dialog'; + selectionDlg.addEventListener('recordclick', startTracing); + selectionDlg.addEventListener('closeclick', cancelRecording); + selectionDlg.visible = true; + + finalPromise.selectionDlg = selectionDlg; + } + + function cancelRecording() { + finalPromise.selectionDlg = undefined; + finalPromiseResolver.reject(new UserCancelledError()); + } + + // Step 2: Do the actual tracing dialog. + let progressDlg; + let bufferPercentFullDiv; + function startTracing() { + progressDlg = new tr.ui.b.Overlay(); + Polymer.dom(progressDlg).textContent = 'Recording...'; + progressDlg.userCanClose = false; + + bufferPercentFullDiv = document.createElement('div'); + Polymer.dom(progressDlg).appendChild(bufferPercentFullDiv); + + const stopButton = document.createElement('button'); + Polymer.dom(stopButton).textContent = 'Stop'; + progressDlg.clickStopButton = function() { + stopButton.click(); + }; + Polymer.dom(progressDlg).appendChild(stopButton); + + const categories = selectionDlg.includedAndExcludedCategories(); + const recordingOptions = { + included_categories: categories.included, + excluded_categories: categories.excluded, + enable_systrace: selectionDlg.useSystemTracing, + record_mode: selectionDlg.tracingRecordMode, + }; + if (categories.included.indexOf( + 'disabled-by-default-memory-infra') !== -1) { + const memoryConfig = { triggers: [] }; + memoryConfig.triggers.push( + {'mode': 'detailed', 'periodic_interval_ms': 10000}); + recordingOptions.memory_dump_config = memoryConfig; + } + + const requestPromise = tracingControllerClient.beginRecording( + recordingOptions); + requestPromise.then( + function() { + progressDlg.visible = true; + stopButton.focus(); + updateBufferPercentFull('0'); + }, + recordFailed); + + stopButton.addEventListener('click', function() { + // TODO(chrishenry): Currently, this only dismiss the progress + // dialog when tracingComplete event is received. When performing + // remote debugging, the tracingComplete event may be delayed + // considerable. We should indicate to user that we are waiting + // for tracingComplete event instead of being unresponsive. (For + // now, I disable the "stop" button, since clicking on the button + // again now cause exception.) + const recordingPromise = endRecording(tracingControllerClient); + recordingPromise.then( + recordFinished, + recordFailed); + stopButton.disabled = true; + bufferPercentFullDiv = undefined; + }); + finalPromise.progressDlg = progressDlg; + } + + function recordFinished(tracedData) { + progressDlg.visible = false; + finalPromise.progressDlg = undefined; + finalPromiseResolver.resolve(tracedData); + } + + function recordFailed(err) { + progressDlg.visible = false; + finalPromise.progressDlg = undefined; + finalPromiseResolver.reject(err); + } + + function getBufferPercentFull() { + if (!bufferPercentFullDiv) return; + + tracingControllerClient.beginGetBufferPercentFull().then( + updateBufferPercentFull); + } + + function updateBufferPercentFull(percentFull) { + if (!bufferPercentFullDiv) return; + + percentFull = Math.round(100 * parseFloat(percentFull)); + const newText = 'Buffer usage: ' + percentFull + '%'; + if (Polymer.dom(bufferPercentFullDiv).textContent !== newText) { + Polymer.dom(bufferPercentFullDiv).textContent = newText; + } + + window.setTimeout(getBufferPercentFull, 500); + } + + // Thats it! We're done. + return finalPromise; + } + + function endRecording(tracingControllerClient) { + return tracingControllerClient.endRecording(); + } + + function defaultTraceName(tracingControllerClient) { + return tracingControllerClient.defaultTraceName(); + } + + function UserCancelledError() { + Error.apply(this, arguments); + } + UserCancelledError.prototype = { + __proto__: Error.prototype + }; + + return { + beginRecording, + UserCancelledError, + defaultTraceName, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html new file mode 100644 index 00000000000..e3e0438f3a2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" + href="/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/record_controller.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const testData = [ + {name: 'a', args: {}, pid: 52, ts: 15000, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'a', args: {}, pid: 52, ts: 19000, cat: 'foo', tid: 53, ph: 'E'}, + {name: 'b', args: {}, pid: 52, ts: 32000, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'b', args: {}, pid: 52, ts: 54000, cat: 'foo', tid: 53, ph: 'E'} + ]; + + test('fullRecording', function() { + const mock = new tr.ui.e.about_tracing.MockTracingControllerClient(); + mock.expectRequest('endRecording', function() { + return ''; + }); + mock.expectRequest('getCategories', function() { + tr.b.timeout(20).then(() => + recordingPromise.selectionDlg.clickRecordButton()); + return ['a', 'b', 'c']; + }); + mock.expectRequest('beginRecording', function(recordingOptions) { + assert.typeOf(recordingOptions.included_categories, 'array'); + assert.typeOf(recordingOptions.excluded_categories, 'array'); + assert.typeOf(recordingOptions.enable_systrace, 'boolean'); + assert.typeOf(recordingOptions.record_mode, 'string'); + tr.b.timeout(10).then(() => + recordingPromise.progressDlg.clickStopButton()); + return ''; + }); + mock.expectRequest('endRecording', function(data) { + return JSON.stringify(testData); + }); + + const recordingPromise = tr.ui.e.about_tracing.beginRecording(mock); + + return recordingPromise.then(function(data) { + mock.assertAllRequestsHandled(); + assert.strictEqual(data, JSON.stringify(testData)); + }, function(error) { + assert.fail(error); + }); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html new file mode 100644 index 00000000000..a5383973a80 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html @@ -0,0 +1,689 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/core/filter.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/info_bar_group.html"> +<link rel="import" href="/tracing/ui/base/overlay.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> + +<template id="record-selection-dialog-template"> + <style> + .categories-column-view { + display: flex; + flex-direction: column; + font-family: sans-serif; + max-width: 640px; + min-height: 0; + min-width: 0; + opacity: 1; + transition: max-height 1s ease, max-width 1s ease, opacity 1s ease; + will-change: opacity; + } + + .categories-column-view-hidden { + max-height: 0; + max-width: 0; + opacity: 0; + overflow: hidden; + display: none; + } + + .categories-selection { + display: flex; + flex-direction: row; + } + + .category-presets { + padding: 4px; + } + + .category-description { + color: #aaa; + font-size: small; + max-height: 1em; + opacity: 1; + padding-left: 4px; + padding-right: 4px; + text-align: right; + transition: max-height 1s ease, opacity 1s ease; + will-change: opacity; + } + + .category-description-hidden { + max-height: 0; + opacity: 0; + } + + .default-enabled-categories, + .default-disabled-categories { + flex: 1 1 auto; + display: flex; + flex-direction: column; + padding: 4px; + width: 300px; + } + + .default-enabled-categories > div, + .default-disabled-categories > div { + padding: 4px; + } + + .tracing-modes { + flex: 1 0 auto; + display: flex; + flex-direction: reverse; + padding: 4px; + border-bottom: 2px solid #ddd; + border-top: 2px solid #ddd; + } + + .default-disabled-categories { + border-left: 2px solid #ddd; + } + + .warning-default-disabled-categories { + display: inline-block; + font-weight: bold; + text-align: center; + color: #BD2E2E; + width: 2.0ex; + height: 2.0ex; + border-radius: 2.0ex; + border: 1px solid #BD2E2E; + } + + .categories { + font-size: 80%; + padding: 10px; + flex: 1 1 auto; + } + + .group-selectors { + font-size: 80%; + border-bottom: 1px solid #ddd; + padding-bottom: 6px; + flex: 0 0 auto; + } + + .group-selectors button { + padding: 1px; + } + + .record-selection-dialog .labeled-option-group { + flex: 0 0 auto; + flex-direction: column; + display: flex; + } + + .record-selection-dialog .labeled-option { + border-top: 5px solid white; + border-bottom: 5px solid white; + } + + .record-selection-dialog .edit-categories { + padding-left: 6px; + } + + .record-selection-dialog .edit-categories:after { + padding-left: 15px; + font-size: 125%; + } + + .record-selection-dialog .labeled-option-group:not(.categories-expanded) + .edit-categories:after { + content: '\25B8'; /* Right triangle */ + } + + .record-selection-dialog .labeled-option-group.categories-expanded + .edit-categories:after { + content: '\25BE'; /* Down triangle */ + } + + </style> + + <div class="record-selection-dialog"> + <tr-ui-b-info-bar-group></tr-ui-b-info-bar-group> + <div class="category-presets"> + </div> + <div class="category-description"></div> + <div class="categories-column-view"> + <div class="tracing-modes"></div> + <div class="categories-selection"> + <div class="default-enabled-categories"> + <div>Record Categories</div> + <div class="group-selectors"> + Select + <button class="all-btn">All</button> + <button class="none-btn">None</button> + </div> + <div class="categories"></div> + </div> + <div class="default-disabled-categories"> + <div>Disabled by Default Categories + <a class="warning-default-disabled-categories">!</a> + </div> + <div class="group-selectors"> + Select + <button class="all-btn">All</button> + <button class="none-btn">None</button> + </div> + <div class="categories"></div> + </div> + </div> + </div> + </div> +</template> + +<script> +'use strict'; + +/** + * @fileoverview RecordSelectionDialog presents the available categories + * to be enabled/disabled during tr.c. + */ +tr.exportTo('tr.ui.e.about_tracing', function() { + const THIS_DOC = document.currentScript.ownerDocument; + const RecordSelectionDialog = tr.ui.b.define('div'); + + const DEFAULT_PRESETS = [ + {title: 'Web developer', + categoryFilter: ['blink', 'cc', 'netlog', 'renderer.scheduler', + 'sequence_manager', 'toplevel', 'v8']}, + {title: 'Input latency', + categoryFilter: ['benchmark', 'input', 'evdev', 'renderer.scheduler', + 'sequence_manager', 'toplevel']}, + {title: 'Rendering', + categoryFilter: ['blink', 'cc', 'gpu', 'toplevel', 'viz']}, + {title: 'Javascript and rendering', + categoryFilter: ['blink', 'cc', 'gpu', 'renderer.scheduler', + 'sequence_manager', 'v8', 'toplevel', 'viz']}, + {title: 'Frame Viewer', + categoryFilter: ['blink', 'cc', 'gpu', 'renderer.scheduler', + 'sequence_manager', 'v8', 'toplevel', + 'disabled-by-default-blink.invalidation', + 'disabled-by-default-cc.debug', + 'disabled-by-default-cc.debug.picture', + 'disabled-by-default-cc.debug.display_items']}, + {title: 'Manually select settings', + categoryFilter: []} + ]; + const RECORDING_MODES = [ + {'label': 'Record until full', + 'value': 'record-until-full'}, + {'label': 'Record continuously', + 'value': 'record-continuously'}, + {'label': 'Record as much as possible', + 'value': 'record-as-much-as-possible'}]; + const DEFAULT_RECORD_MODE = 'record-until-full'; + const DEFAULT_CONTINUOUS_TRACING = true; + const DEFAULT_SYSTEM_TRACING = true; + const DEFAULT_SAMPLING_TRACING = false; + + RecordSelectionDialog.prototype = { + __proto__: tr.ui.b.Overlay.prototype, + + decorate() { + tr.ui.b.Overlay.prototype.decorate.call(this); + this.title = 'Record a new trace...'; + + Polymer.dom(this).classList.add('record-dialog-overlay'); + + const node = + tr.ui.b.instantiateTemplate('#record-selection-dialog-template', + THIS_DOC); + Polymer.dom(this).appendChild(node); + + this.recordButtonEl_ = document.createElement('button'); + Polymer.dom(this.recordButtonEl_).textContent = 'Record'; + this.recordButtonEl_.addEventListener( + 'click', + this.onRecordButtonClicked_.bind(this)); + this.recordButtonEl_.style.fontSize = '110%'; + Polymer.dom(this.buttons).appendChild(this.recordButtonEl_); + + this.categoriesView_ = Polymer.dom(this).querySelector( + '.categories-column-view'); + this.presetsEl_ = Polymer.dom(this).querySelector('.category-presets'); + Polymer.dom(this.presetsEl_).appendChild(tr.ui.b.createOptionGroup( + this, 'currentlyChosenPreset', + 'about_tracing.record_selection_dialog_preset', + DEFAULT_PRESETS[0].categoryFilter, + DEFAULT_PRESETS.map(function(p) { + return { label: p.title, value: p.categoryFilter }; + }))); + + this.tracingRecordModeSltr_ = tr.ui.b.createSelector( + this, 'tracingRecordMode', + 'recordSelectionDialog.tracingRecordMode', + DEFAULT_RECORD_MODE, RECORDING_MODES); + + this.systemTracingBn_ = tr.ui.b.createCheckBox( + undefined, undefined, + 'recordSelectionDialog.useSystemTracing', DEFAULT_SYSTEM_TRACING, + 'System tracing'); + this.samplingTracingBn_ = tr.ui.b.createCheckBox( + undefined, undefined, + 'recordSelectionDialog.useSampling', DEFAULT_SAMPLING_TRACING, + 'State sampling'); + this.tracingModesContainerEl_ = Polymer.dom(this).querySelector( + '.tracing-modes'); + Polymer.dom(this.tracingModesContainerEl_).appendChild( + this.tracingRecordModeSltr_); + Polymer.dom(this.tracingModesContainerEl_).appendChild( + this.systemTracingBn_); + Polymer.dom(this.tracingModesContainerEl_).appendChild( + this.samplingTracingBn_); + + this.enabledCategoriesContainerEl_ = + Polymer.dom(this).querySelector( + '.default-enabled-categories .categories'); + + this.disabledCategoriesContainerEl_ = + Polymer.dom(this).querySelector( + '.default-disabled-categories .categories'); + + this.createGroupSelectButtons_( + Polymer.dom(this).querySelector('.default-enabled-categories')); + this.createGroupSelectButtons_( + Polymer.dom(this).querySelector('.default-disabled-categories')); + this.createDefaultDisabledWarningDialog_( + Polymer.dom(this).querySelector( + '.warning-default-disabled-categories')); + this.editCategoriesOpened_ = false; + + // TODO(chrishenry): When used with tr.ui.b.Overlay (such as in + // chrome://tracing, this does not yet look quite right due to + // the 10px overlay content padding (but it's good enough). + this.infoBarGroup_ = Polymer.dom(this).querySelector( + 'tr-ui-b-info-bar-group'); + + this.addEventListener('visible-change', this.onVisibleChange_.bind(this)); + }, + + set supportsSystemTracing(s) { + if (s) { + this.systemTracingBn_.style.display = undefined; + } else { + this.systemTracingBn_.style.display = 'none'; + this.useSystemTracing = false; + } + }, + + get tracingRecordMode() { + return this.tracingRecordModeSltr_.selectedValue; + }, + set tracingRecordMode(value) { + this.tracingRecordMode_ = value; + }, + + get useSystemTracing() { + return this.systemTracingBn_.checked; + }, + set useSystemTracing(value) { + this.systemTracingBn_.checked = !!value; + }, + + get useSampling() { + return this.samplingTracingBn_.checked; + }, + set useSampling(value) { + this.samplingTracingBn_.checked = !!value; + }, + + set categories(c) { + if (!(c instanceof Array)) { + throw new Error('categories must be an array'); + } + this.categories_ = c; + + for (let i = 0; i < this.categories_.length; i++) { + const split = this.categories_[i].split(','); + this.categories_[i] = split.shift(); + if (split.length > 0) { + this.categories_ = this.categories_.concat(split); + } + } + }, + + set settings_key(k) { + this.settings_key_ = k; + }, + + set settings(s) { + throw new Error('Dont use this!'); + }, + + usingPreset_() { + return this.currentlyChosenPreset_.length > 0; + }, + + get currentlyChosenPreset() { + return this.currentlyChosenPreset_; + }, + + set currentlyChosenPreset(preset) { + if (!(preset instanceof Array)) { + throw new Error('RecordSelectionDialog.currentlyChosenPreset:' + + ' preset must be an array.'); + } + this.currentlyChosenPreset_ = preset; + + if (this.usingPreset_()) { + this.changeEditCategoriesState_(false); + } else { + this.updateCategoryColumnView_(true); + this.changeEditCategoriesState_(true); + } + this.updateManualSelectionView_(); + this.updatePresetDescription_(); + }, + + updateManualSelectionView_() { + const classList = Polymer.dom(this.categoriesView_).classList; + if (!this.usingPreset_()) { + classList.remove('categories-column-view-hidden'); + } else { + if (this.editCategoriesOpened_) { + classList.remove('categories-column-view-hidden'); + } else { + classList.add('categories-column-view-hidden'); + } + } + }, + + updateCategoryColumnView_(shouldReadFromSettings) { + const categorySet = Polymer.dom(this).querySelectorAll('.categories'); + for (let i = 0; i < categorySet.length; ++i) { + const categoryGroup = categorySet[i].children; + for (let j = 0; j < categoryGroup.length; ++j) { + const categoryEl = categoryGroup[j].children[0]; + categoryEl.checked = shouldReadFromSettings ? + tr.b.Settings.get(categoryEl.value, false, this.settings_key_) : + false; + } + } + }, + + onClickEditCategories() { + if (!this.usingPreset_()) return; + + if (!this.editCategoriesOpened_) { + this.updateCategoryColumnView_(false); + for (let i = 0; i < this.currentlyChosenPreset_.length; ++i) { + const categoryEl = this.querySelector('#' + + this.currentlyChosenPreset_[i]); + if (!categoryEl) continue; + categoryEl.checked = true; + } + } + + this.changeEditCategoriesState_(!this.editCategoriesOpened_); + this.updateManualSelectionView_(); + this.recordButtonEl_.focus(); + }, + + changeEditCategoriesState_(editCategoriesState) { + const presetOptionsGroup = Polymer.dom(this).querySelector( + '.labeled-option-group'); + if (!presetOptionsGroup) return; + + this.editCategoriesOpened_ = editCategoriesState; + if (this.editCategoriesOpened_) { + Polymer.dom(presetOptionsGroup).classList.add('categories-expanded'); + } else { + Polymer.dom(presetOptionsGroup).classList.remove( + 'categories-expanded'); + } + }, + + updatePresetDescription_() { + const description = Polymer.dom(this).querySelector( + '.category-description'); + if (this.usingPreset_()) { + description.innerText = this.currentlyChosenPreset_; + Polymer.dom(description).classList.remove( + 'category-description-hidden'); + } else { + description.innerText = ''; + if (!Polymer.dom(description).classList.contains( + 'category-description-hidden')) { + Polymer.dom(description).classList.add('category-description-hidden'); + } + } + }, + + includedAndExcludedCategories() { + let includedCategories = []; + let excludedCategories = []; + if (this.usingPreset_()) { + const allCategories = this.allCategories_(); + for (const category in allCategories) { + const disabledByDefault = + category.indexOf('disabled-by-default-') === 0; + if (this.currentlyChosenPreset_.indexOf(category) >= 0) { + if (disabledByDefault) { + includedCategories.push(category); + } + } else { + if (!disabledByDefault) { + excludedCategories.push(category); + } + } + } + return { + included: includedCategories, + excluded: excludedCategories + }; + } + + excludedCategories = this.unselectedCategories_(); + includedCategories = this.enabledDisabledByDefaultCategories_(); + return { + included: includedCategories, + excluded: excludedCategories + }; + }, + + clickRecordButton() { + this.recordButtonEl_.click(); + }, + + onRecordButtonClicked_() { + this.visible = false; + tr.b.dispatchSimpleEvent(this, 'recordclick'); + return false; + }, + + collectInputs_(inputs, isChecked) { + const inputsLength = inputs.length; + const categories = []; + for (let i = 0; i < inputsLength; ++i) { + const input = inputs[i]; + if (input.checked === isChecked) { + categories.push(input.value); + } + } + return categories; + }, + + unselectedCategories_() { + const inputs = + Polymer.dom(this.enabledCategoriesContainerEl_).querySelectorAll( + 'input'); + return this.collectInputs_(inputs, false); + }, + + enabledDisabledByDefaultCategories_() { + const inputs = + Polymer.dom(this.disabledCategoriesContainerEl_).querySelectorAll( + 'input'); + return this.collectInputs_(inputs, true); + }, + + onVisibleChange_() { + if (this.visible) { + this.updateForm_(); + } + }, + + buildInputs_(inputs, checkedDefault, parent) { + const inputsLength = inputs.length; + for (let i = 0; i < inputsLength; i++) { + const category = inputs[i]; + + const inputEl = document.createElement('input'); + inputEl.type = 'checkbox'; + inputEl.id = category; + inputEl.value = category; + + inputEl.checked = tr.b.Settings.get( + category, checkedDefault, this.settings_key_); + inputEl.onclick = this.updateSetting_.bind(this); + + const labelEl = document.createElement('label'); + Polymer.dom(labelEl).textContent = + category.replace('disabled-by-default-', ''); + Polymer.dom(labelEl).setAttribute('for', category); + + const divEl = document.createElement('div'); + Polymer.dom(divEl).appendChild(inputEl); + Polymer.dom(divEl).appendChild(labelEl); + + Polymer.dom(parent).appendChild(divEl); + } + }, + + allCategories_() { + // Dedup the categories. We may have things in settings that are also + // returned when we query the category list. + const categorySet = {}; + const allCategories = + this.categories_.concat(tr.b.Settings.keys(this.settings_key_)); + const allCategoriesLength = allCategories.length; + for (let i = 0; i < allCategoriesLength; ++i) { + categorySet[allCategories[i]] = true; + } + return categorySet; + }, + + updateForm_() { + function ignoreCaseCompare(a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + } + + // Clear old categories + Polymer.dom(this.enabledCategoriesContainerEl_).innerHTML = ''; + Polymer.dom(this.disabledCategoriesContainerEl_).innerHTML = ''; + + this.recordButtonEl_.focus(); + + const allCategories = this.allCategories_(); + let categories = []; + let disabledCategories = []; + for (const category in allCategories) { + if (category.indexOf('disabled-by-default-') === 0) { + disabledCategories.push(category); + } else { + categories.push(category); + } + } + disabledCategories = disabledCategories.sort(ignoreCaseCompare); + categories = categories.sort(ignoreCaseCompare); + + if (this.categories_.length === 0) { + this.infoBarGroup_.addMessage( + 'No categories found; recording will use default categories.'); + } + + this.buildInputs_(categories, true, this.enabledCategoriesContainerEl_); + + if (disabledCategories.length > 0) { + this.disabledCategoriesContainerEl_.hidden = false; + this.buildInputs_(disabledCategories, false, + this.disabledCategoriesContainerEl_); + } + }, + + updateSetting_(e) { + const checkbox = e.target; + tr.b.Settings.set(checkbox.value, checkbox.checked, this.settings_key_); + + // Change the current record mode to 'Manually select settings' from + // preset mode if and only if currently user is in preset record mode + // and user selects/deselects any category in 'Edit Categories' mode. + if (this.usingPreset_()) { + this.currentlyChosenPreset_ = []; /* manually select settings */ + const categoryEl = this.querySelector( + '#category-preset-Manually-select-settings'); + categoryEl.checked = true; + const description = Polymer.dom(this).querySelector( + '.category-description'); + description.innerText = ''; + Polymer.dom(description).classList.add('category-description-hidden'); + } + }, + + createGroupSelectButtons_(parent) { + const flipInputs = function(dir) { + const inputs = Polymer.dom(parent).querySelectorAll('input'); + for (let i = 0; i < inputs.length; i++) { + if (inputs[i].checked === dir) continue; + // click() is used so the settings will be correclty stored. Setting + // checked does not trigger the onclick (or onchange) callback. + inputs[i].click(); + } + }; + + const allBtn = Polymer.dom(parent).querySelector('.all-btn'); + allBtn.onclick = function(evt) { + flipInputs(true); + evt.preventDefault(); + }; + + const noneBtn = Polymer.dom(parent).querySelector('.none-btn'); + noneBtn.onclick = function(evt) { + flipInputs(false); + evt.preventDefault(); + }; + }, + + setWarningDialogOverlayText_(messages) { + const contentDiv = document.createElement('div'); + + for (let i = 0; i < messages.length; ++i) { + const messageDiv = document.createElement('div'); + Polymer.dom(messageDiv).textContent = messages[i]; + Polymer.dom(contentDiv).appendChild(messageDiv); + } + Polymer.dom(this.warningOverlay_).textContent = ''; + Polymer.dom(this.warningOverlay_).appendChild(contentDiv); + }, + + createDefaultDisabledWarningDialog_(warningLink) { + function onClickHandler(evt) { + this.warningOverlay_ = tr.ui.b.Overlay(); + this.warningOverlay_.parentEl_ = this; + this.warningOverlay_.title = 'Warning...'; + this.warningOverlay_.userCanClose = true; + this.warningOverlay_.visible = true; + + this.setWarningDialogOverlayText_([ + 'Enabling the default disabled categories may have', + 'performance and memory impact while tr.c.' + ]); + + evt.preventDefault(); + } + warningLink.onclick = onClickHandler.bind(this); + } + }; + + return { + RecordSelectionDialog, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html new file mode 100644 index 00000000000..7c62b487305 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html @@ -0,0 +1,426 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/extras/about_tracing/record_selection_dialog.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantitate', function() { + const showButton = document.createElement('button'); + Polymer.dom(showButton).textContent = 'Show record selection dialog'; + this.addHTMLOutput(showButton); + + showButton.addEventListener('click', function(e) { + e.stopPropagation(); + + const categories = []; + for (let i = 0; i < 30; i++) { + categories.push('cat-' + i); + } + for (let i = 0; i < 20; i++) { + categories.push('disabled-by-default-cat-' + i); + } + categories.push( + 'really-really-really-really-really-really-very-loong-cat'); + categories.push('first,second,third'); + categories.push('cc,disabled-by-default-cc.debug'); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = categories; + dlg.settings_key = 'key'; + dlg.visible = true; + }); + }); + + test('recordSelectionDialog_splitCategories', function() { + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = + ['cc,disabled-by-default-one,cc.debug', 'two,three', 'three']; + dlg.settings_key = 'key'; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + + const expected = + ['"cc"', '"cc.debug"', '"disabled-by-default-one"', '"three"', '"two"']; + + const labels = Polymer.dom(dlg).querySelectorAll('.categories input'); + let results = []; + for (let i = 0; i < labels.length; i++) { + results.push('"' + labels[i].value + '"'); + } + results = results.sort(); + + assert.deepEqual(results, expected); + }); + + test('recordSelectionDialog_UpdateForm_NoSettings', function() { + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one', 'two', 'three']; + dlg.settings_key = 'key'; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + + const checkboxes = Polymer.dom(dlg).querySelectorAll('.categories input'); + assert.strictEqual(checkboxes.length, 3); + assert.strictEqual(checkboxes[0].id, 'three'); + assert.strictEqual(checkboxes[0].value, 'three'); + assert.isTrue(checkboxes[0].checked); + assert.strictEqual(checkboxes[1].id, 'two'); + assert.strictEqual(checkboxes[1].value, 'two'); + assert.isTrue(checkboxes[1].checked); + assert.strictEqual(checkboxes[2].id, 'disabled-by-default-one'); + assert.strictEqual(checkboxes[2].value, 'disabled-by-default-one'); + assert.isFalse(checkboxes[2].checked); + + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + + const labels = Polymer.dom(dlg).querySelectorAll('.categories label'); + assert.strictEqual(labels.length, 3); + assert.strictEqual(Polymer.dom(labels[0]).textContent, 'three'); + assert.strictEqual(Polymer.dom(labels[1]).textContent, 'two'); + assert.strictEqual(Polymer.dom(labels[2]).textContent, 'one'); + }); + + test('recordSelectionDialog_UpdateForm_Settings', function() { + tr.b.Settings.set('two', true, 'categories'); + tr.b.Settings.set('three', false, 'categories'); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one']; + dlg.settings_key = 'categories'; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + + const checkboxes = Polymer.dom(dlg).querySelectorAll('.categories input'); + assert.strictEqual(checkboxes.length, 3); + assert.strictEqual(checkboxes[0].id, 'three'); + assert.strictEqual(checkboxes[0].value, 'three'); + assert.isFalse(checkboxes[0].checked); + assert.strictEqual(checkboxes[1].id, 'two'); + assert.strictEqual(checkboxes[1].value, 'two'); + assert.isTrue(checkboxes[1].checked); + assert.strictEqual(checkboxes[2].id, 'disabled-by-default-one'); + assert.strictEqual(checkboxes[2].value, 'disabled-by-default-one'); + assert.isFalse(checkboxes[2].checked); + + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, ['three']); + + const labels = Polymer.dom(dlg).querySelectorAll('.categories label'); + assert.strictEqual(labels.length, 3); + assert.strictEqual(Polymer.dom(labels[0]).textContent, 'three'); + assert.strictEqual(Polymer.dom(labels[1]).textContent, 'two'); + assert.strictEqual(Polymer.dom(labels[2]).textContent, 'one'); + }); + + test('recordSelectionDialog_UpdateForm_DisabledByDefault', function() { + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-bar', 'baz']; + dlg.settings_key = 'categories'; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + + const inputs = + Polymer.dom(dlg).querySelector('input#disabled-by-default-bar').click(); + + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-bar']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + + assert.isFalse( + tr.b.Settings.get('disabled-by-default-foo', false, 'categories')); + }); + + test('selectAll', function() { + tr.b.Settings.set('two', true, 'categories'); + tr.b.Settings.set('three', false, 'categories'); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one']; + dlg.settings_key = 'categories'; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + }); + + test('selectNone', function() { + tr.b.Settings.set('two', true, 'categories'); + tr.b.Settings.set('three', false, 'categories'); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one']; + dlg.settings_key = 'categories'; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + + // Enables the three option, two already enabled. + Polymer.dom(dlg).querySelector('.default-enabled-categories .all-btn') + .click(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + assert.isTrue(tr.b.Settings.get('three', false, 'categories')); + + // Disables three and two. + Polymer.dom(dlg).querySelector('.default-enabled-categories .none-btn') + .click(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['three', 'two']); + assert.isFalse(tr.b.Settings.get('two', false, 'categories')); + assert.isFalse(tr.b.Settings.get('three', false, 'categories')); + + // Turn categories back on so they can be ignored. + Polymer.dom(dlg).querySelector('.default-enabled-categories .all-btn') + .click(); + + // Enables disabled category. + Polymer.dom(dlg).querySelector('.default-disabled-categories .all-btn') + .click(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-one']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + assert.isTrue( + tr.b.Settings.get('disabled-by-default-one', false, 'categories')); + + // Turn disabled by default back off. + Polymer.dom(dlg).querySelector('.default-disabled-categories .none-btn') + .click(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + assert.isFalse( + tr.b.Settings.get('disabled-by-default-one', false, 'categories')); + }); + + test('recordSelectionDialog_noPreset', function() { + tr.b.Settings.set('about_tracing.record_selection_dialog_preset', []); + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + assert.isFalse(dlg.usingPreset_()); + }); + + test('recordSelectionDialog_defaultPreset', function() { + tr.b.Settings.set('two', true, 'categories'); + tr.b.Settings.set('three', false, 'categories'); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one']; + dlg.settings_key = 'categories'; + // Note: currentlyChosenPreset is not set here, so the default is used. + dlg.updateForm_(); + + // Make sure the default filter is returned. + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['three', 'two']); + + // Make sure the default tracing types are returned. + assert.strictEqual(dlg.tracingRecordMode, 'record-until-full'); + assert.isTrue(dlg.useSystemTracing); + assert.isFalse(dlg.useSampling); + + // Make sure the manual settings are not visible. + const classList = Polymer.dom(dlg.categoriesView_).classList; + assert.isTrue(classList.contains('categories-column-view-hidden')); + + // Verify manual settings do not modify the checkboxes. + const checkboxes = Polymer.dom(dlg).querySelectorAll('.categories input'); + assert.strictEqual(checkboxes.length, 3); + assert.strictEqual(checkboxes[0].id, 'three'); + assert.strictEqual(checkboxes[0].value, 'three'); + assert.isFalse(checkboxes[0].checked); + assert.strictEqual(checkboxes[1].id, 'two'); + assert.strictEqual(checkboxes[1].value, 'two'); + assert.isTrue(checkboxes[1].checked); + assert.strictEqual(checkboxes[2].id, 'disabled-by-default-one'); + assert.strictEqual(checkboxes[2].value, 'disabled-by-default-one'); + assert.isFalse(checkboxes[2].checked); + }); + + test('recordSelectionDialog_editPreset', function() { + function createDialog() { + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['one', 'two', 'disabled-by-default-three']; + dlg.settings_key = 'categories'; + // Note: currentlyChosenPreset is not set here, so the default is used. + dlg.updateForm_(); + return dlg; + } + + // After the dialog is created, it should be using the default preset. + let dlg = createDialog(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['one', 'two']); + assert.isTrue(dlg.usingPreset_()); + assert.isFalse( + dlg.querySelector('#category-preset-Manually-select-settings').checked); + + // After clicking on "Edit Categories", the default preset should still be + // used. + dlg.onClickEditCategories(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['one', 'two']); + assert.isTrue(dlg.usingPreset_()); + assert.isFalse( + dlg.querySelector('#category-preset-Manually-select-settings').checked); + + // After clicking on category checkbox(es), the mode should be changed to + // "Manually select settings". + Array.prototype.forEach.call(dlg.querySelectorAll('.categories input'), + checkbox => checkbox.click()); + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-three']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + assert.isFalse(dlg.usingPreset_()); + assert.isTrue( + dlg.querySelector('#category-preset-Manually-select-settings').checked); + + // After the dialog is opened again, it should be using the default preset. + // More importantly, the default preset should NOT be modified. + dlg = createDialog(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['one', 'two']); + assert.isTrue(dlg.usingPreset_()); + assert.isFalse( + dlg.querySelector('#category-preset-Manually-select-settings').checked); + }); + + test('recordSelectionDialog_changePresets', function() { + tr.b.Settings.set('two', true, 'categories'); + tr.b.Settings.set('three', false, 'categories'); + tr.b.Settings.set('disabled-by-default-cc.debug', true, 'categories'); + tr.b.Settings.set('recordSelectionDialog.tracingRecordMode', + 'record-as-much-as-possible'); + tr.b.Settings.set('recordSelectionDialog.useSystemTracing', true); + tr.b.Settings.set('recordSelectionDialog.useSampling', false); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one']; + dlg.settings_key = 'categories'; + // Note: currentlyChosenPreset is not set here, so the default is used. + dlg.updateForm_(); + + // Preset mode is on. + assert.isTrue(dlg.usingPreset_()); + + // Make sure the default filter is returned. + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['three', 'two']); + + // Make sure the default tracing types are returned. + assert.strictEqual(dlg.tracingRecordMode, 'record-as-much-as-possible'); + assert.isTrue(dlg.useSystemTracing); + assert.isFalse(dlg.useSampling); + + // Make sure the manual settings are not visible. + const classList = Polymer.dom(dlg.categoriesView_).classList; + assert.isTrue(classList.contains('categories-column-view-hidden')); + + // Switch to manual settings and verify the default values are not returned. + dlg.currentlyChosenPreset = []; + + // Preset mode is off. + assert.isFalse(dlg.usingPreset_()); + + // Make sure the default filter is returned. + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-cc.debug']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, ['three']); + + // Make sure the tracing types set by catalog are returned. + assert.strictEqual(dlg.tracingRecordMode, 'record-as-much-as-possible'); + assert.isTrue(dlg.useSystemTracing); + assert.isFalse(dlg.useSampling); + assert.isFalse(classList.contains('categories-column-view-hidden')); + + // Switch to the graphics, rendering, and rasterization preset. + dlg.currentlyChosenPreset = ['blink', 'cc', 'renderer', + 'disabled-by-default-cc.debug']; + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-cc.debug']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['three', 'two']); + }); + + test('recordSelectionDialog_savedPreset', function() { + tr.b.Settings.set('two', true, 'categories'); + tr.b.Settings.set('three', false, 'categories'); + tr.b.Settings.set('recordSelectionDialog.tracingRecordMode', + 'record-continuously'); + tr.b.Settings.set('recordSelectionDialog.useSystemTracing', true); + tr.b.Settings.set('recordSelectionDialog.useSampling', true); + tr.b.Settings.set('tr.ui.e.about_tracing.record_selection_dialog_preset', + ['blink', 'cc', 'renderer', 'cc.debug']); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one']; + dlg.settings_key = 'categories'; + dlg.updateForm_(); + + // Make sure the correct filter is returned. + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['three', 'two']); + + // Make sure the correct tracing types are returned. + assert.strictEqual(dlg.tracingRecordMode, 'record-continuously'); + assert.isTrue(dlg.useSystemTracing); + assert.isTrue(dlg.useSampling); + + // Make sure the manual settings are not visible. + const classList = Polymer.dom(dlg.categoriesView_).classList; + assert.isTrue(classList.contains('categories-column-view-hidden')); + + // Switch to manual settings and verify the default values are not returned. + dlg.currentlyChosenPreset = []; + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, ['three']); + assert.strictEqual(dlg.tracingRecordMode, 'record-continuously'); + assert.isTrue(dlg.useSystemTracing); + assert.isTrue(dlg.useSampling); + assert.isFalse(classList.contains('categories-column-view-hidden')); + }); + + test('recordSelectionDialog_categoryFilters', function() { + tr.b.Settings.set('default1', true, 'categories'); + tr.b.Settings.set('disabled1', false, 'categories'); + tr.b.Settings.set('disabled-by-default-cc.disabled2', false, 'categories'); + tr.b.Settings.set('input', true, 'categories'); + tr.b.Settings.set('blink', true, 'categories'); + tr.b.Settings.set('cc', false, 'categories'); + tr.b.Settings.set('disabled-by-default-cc.debug', true, 'categories'); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.settings_key = 'categories'; + dlg.categories = []; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-cc.debug']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['cc', 'disabled1']); + + // Switch to the graphics, rendering, and rasterization preset. + dlg.currentlyChosenPreset = ['blink', 'cc', 'renderer', + 'disabled-by-default-cc.debug']; + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-cc.debug']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['default1', 'disabled1', 'input']); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html new file mode 100644 index 00000000000..c00bbe915e4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<script> +'use strict'; + +tr.exportTo('tr.ui.e.about_tracing', function() { + /** + * Communicates with content/browser/tracing_controller_impl.cc + * + * @constructor + */ + class TracingControllerClient { + beginMonitoring(monitoringOptions) { } + endMonitoring() { } + captureMonitoring() { } + getMonitoringStatus() { } + getCategories() { } + beginRecording(recordingOptions) { } + beginGetBufferPercentFull() { } + endRecording() { } + defaultTraceName() { } + } + + return { + TracingControllerClient, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html new file mode 100644 index 00000000000..d2c6adcac2a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html @@ -0,0 +1,115 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base64.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/tracing_controller_client.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.about_tracing', function() { + const Base64 = tr.b.Base64; + + function beginXhr(method, path, data) { + if (data === undefined) data = null; + + return new Promise(function(resolve, reject) { + const req = new XMLHttpRequest(); + if (method !== 'POST' && data !== null) { + throw new Error('Non-POST should have data==null'); + } + req.open(method, path, true); + req.onreadystatechange = function(e) { + if (req.readyState === 4) { + window.setTimeout(function() { + if (req.status === 200 && req.responseText !== '##ERROR##') { + resolve(req.responseText); + } else { + reject(new Error('Error occured at ' + path)); + } + }, 0); + } + }; + req.send(data); + }); + } + + /** + * @constructor + */ + function XhrBasedTracingControllerClient() { } + + XhrBasedTracingControllerClient.prototype = { + __proto__: tr.ui.e.about_tracing.TracingControllerClient.prototype, + + beginMonitoring(monitoringOptions) { + const monitoringOptionsB64 = Base64.btoa(JSON.stringify( + monitoringOptions)); + return beginXhr('GET', '/json/begin_monitoring?' + monitoringOptionsB64); + }, + + endMonitoring() { + return beginXhr('GET', '/json/end_monitoring'); + }, + + captureMonitoring() { + return beginXhr('GET', '/json/capture_monitoring_compressed').then( + function(data) { + const decodedSize = Base64.getDecodedBufferLength(data); + const buffer = new ArrayBuffer(decodedSize); + Base64.DecodeToTypedArray(data, new DataView(buffer)); + return buffer; + } + ); + }, + + getMonitoringStatus() { + return beginXhr('GET', '/json/get_monitoring_status').then( + function(monitoringOptionsB64) { + return JSON.parse(Base64.atob(monitoringOptionsB64)); + }); + }, + + getCategories() { + return beginXhr('GET', '/json/categories').then( + function(json) { + return JSON.parse(json); + }); + }, + + beginRecording(recordingOptions) { + const recordingOptionsB64 = Base64.btoa(JSON.stringify(recordingOptions)); + return beginXhr('GET', '/json/begin_recording?' + + recordingOptionsB64); + }, + + beginGetBufferPercentFull() { + return beginXhr('GET', '/json/get_buffer_percent_full'); + }, + + endRecording() { + return beginXhr('GET', '/json/end_recording_compressed').then( + function(data) { + const decodedSize = Base64.getDecodedBufferLength(data); + const buffer = new ArrayBuffer(decodedSize); + Base64.DecodeToTypedArray(data, new DataView(buffer)); + return buffer; + } + ); + }, + + defaultTraceName() { + return 'trace.json.gz'; + } + }; + + return { + XhrBasedTracingControllerClient, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html new file mode 100644 index 00000000000..79ba7e593c0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/chrome/cc/cc.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_list_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_selection.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/tile_view.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html new file mode 100644 index 00000000000..f8bfd671355 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html @@ -0,0 +1,451 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base64.html"> +<link rel="import" href="/tracing/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> +<link rel="import" href="/tracing/ui/base/info_bar.html"> +<link rel="import" href="/tracing/ui/base/list_view.html"> +<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html"> +<link rel="import" href="/tracing/ui/base/overlay.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_list_item.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_list_view.html"> + +<template id="tr-ui-e-chrome-cc-display-item-debugger-template"> + <left-panel> + <display-item-info> + <header> + <span class='title'>Display Item List</span> + <span class='size'></span> + <div class='export'> + <input class='dlfilename' type='text' value='displayitemlist.json' /> + <button class='dlexport'>Export display item list</button> + </div> + <div class='export'> + <input class='skpfilename' type='text' value='skpicture.skp' /> + <button class='skpexport'>Export list as SkPicture</button> + </div> + </header> + </display-item-info> + </left-panel> + <right-panel> + <raster-area> + <canvas-scroller> + <canvas></canvas> + </canvas-scroller> + </raster-area> + </right-panel> +</template> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const THIS_DOC = document.currentScript.ownerDocument; + + /** + * DisplayItemDebugger is a view of a DisplayItemListSnapshot for inspecting + * a display item list and the pictures within it. + * + * @constructor + */ + const DisplayItemDebugger = tr.ui.b.define( + 'tr-ui-e-chrome-cc-display-item-debugger'); + + DisplayItemDebugger.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + const node = tr.ui.b.instantiateTemplate( + '#tr-ui-e-chrome-cc-display-item-debugger-template', THIS_DOC); + + Polymer.dom(this).appendChild(node); + this.style.flexGrow = 1; + this.style.flexShrink = 1; + this.style.flexBasis = 'auto'; + this.style.display = 'flex'; + this.style.minWidth = 0; + + this.pictureAsImageData_ = undefined; + this.zoomScaleValue_ = 1; + + this.sizeInfo_ = Polymer.dom(this).querySelector('.size'); + this.rasterArea_ = Polymer.dom(this).querySelector('raster-area'); + this.rasterArea_.style.flexGrow = 1; + this.rasterArea_.style.flexShrink = 1; + this.rasterArea_.style.flexBasis = 'auto'; + this.rasterArea_.style.backgroundColor = '#ddd'; + this.rasterArea_.style.minHeight = '200px'; + this.rasterArea_.style.minWidth = '200px'; + this.rasterArea_.style.paddingLeft = '5px'; + this.rasterArea_.style.display = 'flex'; + this.rasterArea_.style.flexDirection = 'column'; + this.rasterCanvas_ = + Polymer.dom(this.rasterArea_).querySelector('canvas'); + this.rasterCtx_ = this.rasterCanvas_.getContext('2d'); + + const canvasScroller = Polymer.dom(this).querySelector('canvas-scroller'); + canvasScroller.style.flexGrow = 1; + canvasScroller.style.flexShrink = 1; + canvasScroller.style.flexBasis = 'auto'; + canvasScroller.style.minWidth = 0; + canvasScroller.style.minHeight = 0; + canvasScroller.style.overflow = 'auto'; + + this.trackMouse_(); + + this.displayItemInfo_ = + Polymer.dom(this).querySelector('display-item-info'); + this.displayItemInfo_.addEventListener( + 'click', this.onDisplayItemInfoClick_.bind(this), false); + + this.displayItemListView_ = new tr.ui.b.ListView(); + this.displayItemListView_.addEventListener('selection-changed', + this.onDisplayItemListSelection_.bind(this)); + Polymer.dom(this.displayItemInfo_).appendChild(this.displayItemListView_); + + this.displayListFilename_ = + Polymer.dom(this).querySelector('.dlfilename'); + this.displayListExportButton_ = + Polymer.dom(this).querySelector('.dlexport'); + this.displayListExportButton_.addEventListener( + 'click', this.onExportDisplayListClicked_.bind(this)); + + this.skpFilename_ = Polymer.dom(this).querySelector('.skpfilename'); + this.skpExportButton_ = Polymer.dom(this).querySelector('.skpexport'); + this.skpExportButton_.addEventListener( + 'click', this.onExportSkPictureClicked_.bind(this)); + + const leftPanel = Polymer.dom(this).querySelector('left-panel'); + leftPanel.style.flexGrow = 0; + leftPanel.style.flexShrink = 0; + leftPanel.style.flexBasis = 'auto'; + leftPanel.style.minWidth = '200px'; + leftPanel.style.overflow = 'auto'; + + leftPanel.children[0].paddingTop = '2px'; + leftPanel.children[0].children[0].style.borderBottom = '1px solid #555'; + + const leftPanelTitle = leftPanel.querySelector('.title'); + leftPanelTitle.style.fontWeight = 'bold'; + leftPanelTitle.style.marginLeft = '5px'; + leftPanelTitle.style.marginright = '5px'; + + for (const div of leftPanel.querySelectorAll('.export')) { + div.style.margin = '5px'; + } + + const middleDragHandle = document.createElement('tr-ui-b-drag-handle'); + middleDragHandle.style.flexGrow = 0; + middleDragHandle.style.flexShrink = 0; + middleDragHandle.style.flexBasis = 'auto'; + middleDragHandle.horizontal = false; + middleDragHandle.target = leftPanel; + + const rightPanel = Polymer.dom(this).querySelector('right-panel'); + rightPanel.style.display = 'flex'; + rightPanel.style.flexGrow = 1; + rightPanel.style.flexShrink = 1; + rightPanel.style.flexBasis = 'auto'; + rightPanel.style.minWidth = 0; + + this.infoBar_ = document.createElement('tr-ui-b-info-bar'); + Polymer.dom(this.rasterArea_).insertBefore(this.infoBar_, canvasScroller); + + Polymer.dom(this).insertBefore(middleDragHandle, rightPanel); + + this.picture_ = undefined; + + this.pictureOpsListView_ = new tr.ui.e.chrome.cc.PictureOpsListView(); + this.pictureOpsListView_.style.flexGrow = 0; + this.pictureOpsListView_.style.flexShrink = 0; + this.pictureOpsListView_.style.flexBasis = 'auto'; + this.pictureOpsListView_.style.overflow = 'auto'; + this.pictureOpsListView_.style.minWidth = '100px'; + Polymer.dom(rightPanel).insertBefore( + this.pictureOpsListView_, this.rasterArea_); + + this.pictureOpsListDragHandle_ = + document.createElement('tr-ui-b-drag-handle'); + this.pictureOpsListDragHandle_.horizontal = false; + this.pictureOpsListDragHandle_.target = this.pictureOpsListView_; + Polymer.dom(rightPanel).insertBefore( + this.pictureOpsListDragHandle_, this.rasterArea_); + }, + + get picture() { + return this.picture_; + }, + + set displayItemList(displayItemList) { + this.displayItemList_ = displayItemList; + this.picture = this.displayItemList_; + + this.displayItemListView_.clear(); + this.displayItemList_.items.forEach(function(item) { + const listItem = document.createElement( + 'tr-ui-e-chrome-cc-display-item-list-item'); + listItem.data = item; + Polymer.dom(this.displayItemListView_).appendChild(listItem); + }.bind(this)); + }, + + set picture(picture) { + this.picture_ = picture; + + // Hide the ops list if we are showing the "main" display item list. + const showOpsList = picture && picture !== this.displayItemList_; + this.updateDrawOpsList_(showOpsList); + + if (picture) { + const size = this.getRasterCanvasSize_(); + this.rasterCanvas_.width = size.width; + this.rasterCanvas_.height = size.height; + } + + const bounds = this.rasterArea_.getBoundingClientRect(); + const selectorBounds = this.mouseModeSelector_.getBoundingClientRect(); + this.mouseModeSelector_.pos = { + x: (bounds.right - selectorBounds.width - 10), + y: bounds.top + }; + + this.rasterize_(); + + this.scheduleUpdateContents_(); + }, + + getRasterCanvasSize_() { + const style = window.getComputedStyle(this.rasterArea_); + let width = parseInt(style.width); + let height = parseInt(style.height); + if (this.picture_) { + width = Math.max(width, this.picture_.layerRect.width); + height = Math.max(height, this.picture_.layerRect.height); + } + + return { + width, + height + }; + }, + + scheduleUpdateContents_() { + if (this.updateContentsPending_) return; + + this.updateContentsPending_ = true; + tr.b.requestAnimationFrameInThisFrameIfPossible( + this.updateContents_.bind(this) + ); + }, + + updateContents_() { + this.updateContentsPending_ = false; + + if (this.picture_) { + Polymer.dom(this.sizeInfo_).textContent = '(' + + this.picture_.layerRect.width + ' x ' + + this.picture_.layerRect.height + ')'; + } + + // Return if picture hasn't finished rasterizing. + if (!this.pictureAsImageData_) return; + + this.infoBar_.visible = false; + this.infoBar_.removeAllButtons(); + if (this.pictureAsImageData_.error) { + this.infoBar_.message = 'Cannot rasterize...'; + this.infoBar_.addButton('More info...', function(e) { + const overlay = new tr.ui.b.Overlay(); + Polymer.dom(overlay).textContent = this.pictureAsImageData_.error; + overlay.visible = true; + e.stopPropagation(); + return false; + }.bind(this)); + this.infoBar_.visible = true; + } + + this.drawPicture_(); + }, + + drawPicture_() { + const size = this.getRasterCanvasSize_(); + if (size.width !== this.rasterCanvas_.width) { + this.rasterCanvas_.width = size.width; + } + if (size.height !== this.rasterCanvas_.height) { + this.rasterCanvas_.height = size.height; + } + + this.rasterCtx_.clearRect(0, 0, size.width, size.height); + + if (!this.picture_ || !this.pictureAsImageData_.imageData) return; + + const imgCanvas = this.pictureAsImageData_.asCanvas(); + const w = imgCanvas.width; + const h = imgCanvas.height; + this.rasterCtx_.drawImage(imgCanvas, 0, 0, w, h, + 0, 0, w * this.zoomScaleValue_, + h * this.zoomScaleValue_); + }, + + rasterize_() { + if (this.picture_) { + this.picture_.rasterize( + { + showOverdraw: false + }, + this.onRasterComplete_.bind(this)); + } + }, + + onRasterComplete_(pictureAsImageData) { + this.pictureAsImageData_ = pictureAsImageData; + this.scheduleUpdateContents_(); + }, + + onDisplayItemListSelection_(e) { + const selected = this.displayItemListView_.selectedElement; + + if (!selected) { + this.picture = this.displayItemList_; + return; + } + + const index = Array.prototype.indexOf.call( + this.displayItemListView_.children, selected); + const displayItem = this.displayItemList_.items[index]; + if (displayItem && displayItem.skp64) { + this.picture = new tr.e.cc.Picture( + displayItem.skp64, this.displayItemList_.layerRect); + } else { + this.picture = undefined; + } + }, + + onDisplayItemInfoClick_(e) { + if (e && e.target === this.displayItemInfo_) { + this.displayItemListView_.selectedElement = undefined; + } + }, + + updateDrawOpsList_(showOpsList) { + if (showOpsList) { + this.pictureOpsListView_.picture = this.picture_; + if (this.pictureOpsListView_.numOps > 0) { + this.pictureOpsListView_.style.display = 'block'; + this.pictureOpsListDragHandle_.style.display = 'block'; + } + } else { + this.pictureOpsListView_.style.display = 'none'; + this.pictureOpsListDragHandle_.style.display = 'none'; + } + }, + + trackMouse_() { + this.mouseModeSelector_ = document.createElement( + 'tr-ui-b-mouse-mode-selector'); + this.mouseModeSelector_.targetElement = this.rasterArea_; + Polymer.dom(this.rasterArea_).appendChild(this.mouseModeSelector_); + + this.mouseModeSelector_.supportedModeMask = + tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM; + this.mouseModeSelector_.mode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM; + this.mouseModeSelector_.defaultMode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM; + this.mouseModeSelector_.settingsKey = 'pictureDebugger.mouseModeSelector'; + + this.mouseModeSelector_.addEventListener('beginzoom', + this.onBeginZoom_.bind(this)); + this.mouseModeSelector_.addEventListener('updatezoom', + this.onUpdateZoom_.bind(this)); + this.mouseModeSelector_.addEventListener('endzoom', + this.onEndZoom_.bind(this)); + }, + + onBeginZoom_(e) { + this.isZooming_ = true; + + this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e); + + e.preventDefault(); + }, + + onUpdateZoom_(e) { + if (!this.isZooming_) return; + + const currentMouseViewPos = this.extractRelativeMousePosition_(e); + + // Take the distance the mouse has moved and we want to zoom at about + // 1/1000th of that speed. 0.01 feels jumpy. This could possibly be tuned + // more if people feel it's too slow. + this.zoomScaleValue_ += + ((this.lastMouseViewPos_.y - currentMouseViewPos.y) * 0.001); + this.zoomScaleValue_ = Math.max(this.zoomScaleValue_, 0.1); + + this.drawPicture_(); + + this.lastMouseViewPos_ = currentMouseViewPos; + }, + + onEndZoom_(e) { + this.lastMouseViewPos_ = undefined; + this.isZooming_ = false; + e.preventDefault(); + }, + + extractRelativeMousePosition_(e) { + return { + x: e.clientX - this.rasterArea_.offsetLeft, + y: e.clientY - this.rasterArea_.offsetTop + }; + }, + + saveFile_(filename, rawData) { + if (!rawData) return; + + // Convert this String into an Uint8Array + const length = rawData.length; + const arrayBuffer = new ArrayBuffer(length); + const uint8Array = new Uint8Array(arrayBuffer); + for (let c = 0; c < length; c++) { + uint8Array[c] = rawData.charCodeAt(c); + } + + // Create a blob URL from the binary array. + const blob = new Blob([uint8Array], {type: 'application/octet-binary'}); + const blobUrl = window.URL.createObjectURL(blob); + + // Create a link and click on it. + const link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); + link.href = blobUrl; + link.download = filename; + const event = document.createEvent('MouseEvents'); + event.initMouseEvent( + 'click', true, false, window, 0, 0, 0, 0, 0, + false, false, false, false, 0, null); + link.dispatchEvent(event); + }, + + onExportDisplayListClicked_() { + const rawData = JSON.stringify(this.displayItemList_.items); + this.saveFile_(this.displayListFilename_.value, rawData); + }, + + onExportSkPictureClicked_() { + const rawData = tr.b.Base64.atob(this.picture_.getBase64SkpData()); + this.saveFile_(this.skpFilename_.value, rawData); + } + }; + + return { + DisplayItemDebugger, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html new file mode 100644 index 00000000000..c10d6995db3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/chrome/cc/display_item_list.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_debugger.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const displayItemList = new tr.e.cc.DisplayItemListSnapshot( + {id: '31415'}, + 10, + { + 'params': { + 'layer_rect': [-15, -15, 46, 833], + 'items': [ + 'BeginClipDisplayItem', + 'EndClipDisplayItem' + ] + }, + 'skp64': '[another skia picture in base64]'}); + displayItemList.preInitialize(); + displayItemList.initialize(); + + const dbg = new tr.ui.e.chrome.cc.DisplayItemDebugger(); + this.addHTMLOutput(dbg); + assert.isUndefined(dbg.displayItemList_); + assert.isUndefined(dbg.picture_); + dbg.displayItemList = displayItemList; + assert.isDefined(dbg.displayItemList_); + assert.isDefined(dbg.picture_); + assert.strictEqual(dbg.displayItemList_.items.length, 2); + dbg.style.border = '1px solid black'; + }); + + test('selections', function() { + const displayItemList = new tr.e.cc.DisplayItemListSnapshot( + {id: '31415'}, + 10, + { + 'params': { + 'layer_rect': [-15, -15, 46, 833], + 'items': [ + 'BeginClipDisplayItem', + 'TransformDisplayItem', + { + 'name': 'DrawingDisplayItem', + 'skp64': '[skia picture in base64]', + }, + 'EndTransformDisplayItem', + 'EndClipDisplayItem' + ] + }, + 'skp64': '[another skia picture in base64]'}); + displayItemList.preInitialize(); + displayItemList.initialize(); + + const dbg = new tr.ui.e.chrome.cc.DisplayItemDebugger(); + this.addHTMLOutput(dbg); + dbg.displayItemList = displayItemList; + assert.isDefined(dbg.displayItemList_); + assert.isDefined(dbg.picture_); + assert.strictEqual(dbg.displayItemList_.items.length, 5); + + const initialPicture = dbg.picture_; + assert.isAbove(initialPicture.guid, 0); + + // Select the drawing display item and make sure the picture updates. + const listView = dbg.displayItemListView_; + listView.selectedElement = listView.getElementByIndex(3); + let updatedPicture = dbg.picture_; + assert.isAbove(updatedPicture.guid, 0); + assert.notEqual(initialPicture.guid, updatedPicture.guid); + + // Select the TransformDisplayItem and make sure the picture is blank. + listView.selectedElement = listView.getElementByIndex(2); + assert.isUndefined(dbg.picture_); + + // Deselect a list item and make sure the picture is reset to the original. + listView.selectedElement = undefined; + updatedPicture = dbg.picture_; + assert.isAbove(updatedPicture.guid, 0); + assert.strictEqual(initialPicture.guid, updatedPicture.guid); + + dbg.style.border = '1px solid black'; + }); + + test('export', function() { + const displayItemList = new tr.e.cc.DisplayItemListSnapshot( + {id: '31415'}, + 10, + { + 'params': { + 'layer_rect': [-15, -15, 46, 833], + 'items': [ + 'BeginClipDisplayItem', + 'EndClipDisplayItem' + ] + }, + 'skp64': 'c2twaWN0dXJl'}); + displayItemList.preInitialize(); + displayItemList.initialize(); + + const dbg = new tr.ui.e.chrome.cc.DisplayItemDebugger(); + this.addHTMLOutput(dbg); + dbg.displayItemList = displayItemList; + + let onSaveDisplayListCalled = false; + dbg.saveFile_ = function(filename, rawData) { + onSaveDisplayListCalled = true; + assert.strictEqual(filename, 'displayitemlist.json'); + assert.strictEqual( + rawData, '["BeginClipDisplayItem","EndClipDisplayItem"]'); + }; + dbg.onExportDisplayListClicked_(); + assert(onSaveDisplayListCalled); + + let onSaveSkPictureCalled = false; + dbg.saveFile_ = function(filename, rawData) { + onSaveSkPictureCalled = true; + assert.strictEqual(filename, 'skpicture.skp'); + assert.strictEqual(rawData, 'skpicture'); + }; + dbg.onExportSkPictureClicked_(); + assert(onSaveSkPictureCalled); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html new file mode 100644 index 00000000000..3024e8d2d22 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<!-- +An element displaying basic information about a display item in a list view. +--> +<dom-module id='tr-ui-e-chrome-cc-display-item-list-item'> + <template> + <style> + :host { + border-bottom: 1px solid #555; + display: block; + font-size: 12px; + padding: 3px 5px; + } + + :host(:hover) { + background-color: #f0f0f0; + cursor: pointer; + } + + .header { + font-weight: bold; + margin: 2px 0; + } + + .header > .extra { + background-color: #777; + border-radius: 4px; + color: white; + margin: 0 6px; + text-decoration: none; + padding: 2px 4px; + } + + .raw-details { + white-space: pre-wrap; + } + + .details > dl { + margin: 0; + } + + :host(:not([selected])) .details { + display: none; + } + </style> + <div class="header"> + {{name}} + <template is="dom-if" if="{{_computeIfSKP(richDetails)}}"> + <a class="extra" href$="{{_computeHref(richDetails)}}" + download="drawing.skp" on-click="{{stopPropagation}}">SKP</a> + </template> + </div> + <div class="details"> + <template is="dom-if" if="{{rawDetails}}"> + <div class="raw-details">{{rawDetails}}</div> + </template> + <template is="dom-if" if="{{richDetails}}"> + <dl> + <template is="dom-if" if="{{richDetails.visualRect}}"> + <dt>Visual rect</dt> + <dd>{{richDetails.visualRect.x}},{{richDetails.visualRect.y}} + {{richDetails.visualRect.width}}×{{richDetails.visualRect.height}} + </dd> + </template> + </dl> + </template> + </div> + </template> +<script> +'use strict'; +(function() { + // Extracts the "type" and "details" parts of the unstructured (plaintext) + // display item format, even if the details span multiple lines. + // For example, given "FooDisplayItem type=hello\nworld", produces + // "FooDisplayItem" as the first capture and "type=hello\nworld" as the + // second. Either capture could be the empty string, but this regex will + // still successfully match. + const DETAILS_SPLIT_REGEX = /^(\S*)\s*([\S\s]*)$/; + + Polymer({ + is: 'tr-ui-e-chrome-cc-display-item-list-item', + + created() { + // TODO(charliea): Why is setAttribute necessary here but not below? We + // should reach out to the Polymer team to figure out. + Polymer.dom(this).setAttribute('name', ''); + Polymer.dom(this).setAttribute('rawDetails', ''); + Polymer.dom(this).setAttribute('richDetails', undefined); + Polymer.dom(this).setAttribute('data_', undefined); + }, + + get data() { + return this.data_; + }, + + set data(data) { + this.data_ = data; + + if (!data) { + this.name = 'DATA MISSING'; + this.rawDetails = ''; + this.richDetails = undefined; + } else if (typeof data === 'string') { + const match = data.match(DETAILS_SPLIT_REGEX); + this.name = match[1]; + this.rawDetails = match[2]; + this.richDetails = undefined; + } else { + this.name = data.name; + this.rawDetails = ''; + this.richDetails = data; + } + }, + + stopPropagation(e) { + e.stopPropagation(); + }, + + _computeIfSKP(richDetails) { + return richDetails && richDetails.skp64; + }, + + _computeHref(richDetails) { + return 'data:application/octet-stream;base64,' + richDetails.skp64; + } + }); +})(); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html new file mode 100644 index 00000000000..97598aaf3a7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/chrome/cc/display_item_list.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_debugger.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + /* + * Displays a display item snapshot in a human readable form. + * @constructor + */ + const DisplayItemSnapshotView = tr.ui.b.define( + 'tr-ui-e-chrome-cc-display-item-list-view', + tr.ui.analysis.ObjectSnapshotView); + + DisplayItemSnapshotView.prototype = { + __proto__: tr.ui.analysis.ObjectSnapshotView.prototype, + + decorate() { + this.style.display = 'flex'; + this.style.flexGrow = 1; + this.style.flexShrink = 1; + this.style.flexBasis = 'auto'; + this.style.minWidth = 0; + this.displayItemDebugger_ = new tr.ui.e.chrome.cc.DisplayItemDebugger(); + this.displayItemDebugger_.style.flexGrow = 1; + this.displayItemDebugger_.style.flexShrink = 1; + this.displayItemDebugger_.style.flexBasis = 'auto'; + this.displayItemDebugger_.style.minWidth = 0; + Polymer.dom(this).appendChild(this.displayItemDebugger_); + }, + + updateContents() { + if (this.objectSnapshot_ && this.displayItemDebugger_) { + this.displayItemDebugger_.displayItemList = this.objectSnapshot_; + } + } + }; + + tr.ui.analysis.ObjectSnapshotView.register( + DisplayItemSnapshotView, + { + typeNames: ['cc::DisplayItemList'], + showInstances: false + }); + + return { + DisplayItemSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.png b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.png Binary files differnew file mode 100644 index 00000000000..a2b7710d3c4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.png diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg new file mode 100644 index 00000000000..00531ac68d7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="744.09448819" + height="1052.3622047" + id="svg2" + version="1.1" + inkscape:version="0.48.4 r9939" + sodipodi:docname="New document 1"> + <defs + id="defs4"> + <filter + inkscape:collect="always" + id="filter3791"> + <feGaussianBlur + inkscape:collect="always" + stdDeviation="2.7246316" + id="feGaussianBlur3793" /> + </filter> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="2.8" + inkscape:cx="195.13782" + inkscape:cy="982.30556" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:window-width="1215" + inkscape:window-height="860" + inkscape:window-x="2219" + inkscape:window-y="113" + inkscape:window-maximized="0" /> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"> + <g + id="g3882" + style="opacity:0.5" + inkscape:export-filename="/tmp/input-event.png" + inkscape:export-xdpi="82.07" + inkscape:export-ydpi="82.07"> + <path + transform="matrix(1.0152631,0,0,1.0152631,-0.71357503,0.46150497)" + sodipodi:type="arc" + style="opacity:0.50934604000000006;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter3791);enable-background:accumulate" + id="path3755" + sodipodi:cx="177.78685" + sodipodi:cy="100.79848" + sodipodi:rx="42.426407" + sodipodi:ry="42.426407" + d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z" /> + <path + transform="translate(-2,-2)" + d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z" + sodipodi:ry="42.426407" + sodipodi:rx="42.426407" + sodipodi:cy="100.79848" + sodipodi:cx="177.78685" + id="path2985" + style="color:#000000;fill:#d4d4d4;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + sodipodi:type="arc" /> + <path + inkscape:connector-curvature="0" + id="path3853" + d="m 175.28125,96.03125 0,8.46875 1,0 0,-8.46875 -1,0 z" + style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" /> + <path + inkscape:connector-curvature="0" + id="path3859" + d="m 171.53125,99.75 0,1 8.46875,0 0,-1 -8.46875,0 z" + style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" /> + </g> + <path + transform="matrix(1.2923213,0,0,1.2923213,-53.970887,-31.465544)" + d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z" + sodipodi:ry="42.426407" + sodipodi:rx="42.426407" + sodipodi:cy="100.79848" + sodipodi:cx="177.78685" + id="path3867" + style="color:#000000;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + sodipodi:type="arc" + inkscape:export-filename="/tmp/input-event.png" + inkscape:export-xdpi="82.07" + inkscape:export-ydpi="82.07" /> + </g> +</svg> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html new file mode 100644 index 00000000000..9f81199e358 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html @@ -0,0 +1,336 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/extras/chrome/cc/constants.html"> +<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html"> +<link rel="import" href="/tracing/extras/chrome/cc/util.html"> +<link rel="import" href="/tracing/model/event.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/base/list_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const constants = tr.e.cc.constants; + const RENDER_PASS_QUADS = + Math.max(constants.ACTIVE_TREE, constants.PENDING_TREE) + 1; + + /** + * @constructor + */ + const LayerPicker = tr.ui.b.define('tr-ui-e-chrome-cc-layer-picker'); + + LayerPicker.prototype = { + __proto__: HTMLUnknownElement.prototype, + + decorate() { + this.lthi_ = undefined; + this.controls_ = document.createElement('top-controls'); + this.renderPassQuads_ = false; + + this.style.display = 'flex'; + this.style.flexDirection = 'column'; + this.controls_.style.flexGrow = 0; + this.controls_.style.flexShrink = 0; + this.controls_.style.flexBasis = 'auto'; + this.controls_.style.backgroundImage = + '-webkit-gradient(linear, 0 0, 100% 0, from(#E5E5E5), to(#D1D1D1))'; + this.controls_.style.borderBottom = '1px solid #8e8e8e'; + this.controls_.style.borderTop = '1px solid white'; + this.controls_.style.display = 'inline'; + this.controls_.style.fontSize = '14px'; + this.controls_.style.paddingLeft = '2px'; + + this.itemList_ = new tr.ui.b.ListView(); + this.itemList_.style.flexGrow = 1; + this.itemList_.style.flexShrink = 1; + this.itemList_.style.flexBasis = 'auto'; + this.itemList_.style.fontFamily = 'monospace'; + this.itemList_.style.overflow = 'auto'; + Polymer.dom(this).appendChild(this.controls_); + + Polymer.dom(this).appendChild(this.itemList_); + + this.itemList_.addEventListener( + 'selection-changed', this.onItemSelectionChanged_.bind(this)); + + Polymer.dom(this.controls_).appendChild(tr.ui.b.createSelector( + this, 'whichTree', + 'layerPicker.whichTree', constants.ACTIVE_TREE, + [{label: 'Active tree', value: constants.ACTIVE_TREE}, + {label: 'Pending tree', value: constants.PENDING_TREE}, + {label: 'Render pass quads', value: RENDER_PASS_QUADS}])); + + this.showPureTransformLayers_ = false; + const showPureTransformLayers = tr.ui.b.createCheckBox( + this, 'showPureTransformLayers', + 'layerPicker.showPureTransformLayers', false, + 'Transform layers'); + Polymer.dom(showPureTransformLayers).classList.add( + 'show-transform-layers'); + showPureTransformLayers.title = + 'When checked, pure transform layers are shown'; + Polymer.dom(this.controls_).appendChild(showPureTransformLayers); + }, + + get lthiSnapshot() { + return this.lthiSnapshot_; + }, + + set lthiSnapshot(lthiSnapshot) { + this.lthiSnapshot_ = lthiSnapshot; + this.updateContents_(); + }, + + get whichTree() { + return this.renderPassQuads_ ? constants.ACTIVE_TREE : this.whichTree_; + }, + + set whichTree(whichTree) { + this.whichTree_ = whichTree; + this.renderPassQuads_ = (whichTree === RENDER_PASS_QUADS); + this.updateContents_(); + tr.b.dispatchSimpleEvent(this, 'selection-change', false); + }, + + get layerTreeImpl() { + if (this.lthiSnapshot === undefined) return undefined; + + return this.lthiSnapshot.getTree(this.whichTree); + }, + + get isRenderPassQuads() { + return this.renderPassQuads_; + }, + + get showPureTransformLayers() { + return this.showPureTransformLayers_; + }, + + set showPureTransformLayers(show) { + if (this.showPureTransformLayers_ === show) return; + + this.showPureTransformLayers_ = show; + this.updateContents_(); + }, + + getRenderPassInfos_() { + if (!this.lthiSnapshot_) return []; + + const renderPassInfo = []; + if (!this.lthiSnapshot_.args.frame || + !this.lthiSnapshot_.args.frame.renderPasses) { + return renderPassInfo; + } + + const renderPasses = this.lthiSnapshot_.args.frame.renderPasses; + for (let i = 0; i < renderPasses.length; ++i) { + const info = {renderPass: renderPasses[i], + depth: 0, + id: i, + name: 'cc::RenderPass'}; + renderPassInfo.push(info); + } + return renderPassInfo; + }, + + getLayerInfos_() { + if (!this.lthiSnapshot_) return []; + + const tree = this.lthiSnapshot_.getTree(this.whichTree_); + if (!tree) return []; + + const layerInfos = []; + + const showPureTransformLayers = this.showPureTransformLayers_; + + const visitedLayers = {}; + function visitLayer(layer, depth, isMask, isReplica) { + if (visitedLayers[layer.layerId]) return; + + visitedLayers[layer.layerId] = true; + const info = {layer, + depth}; + + if (layer.args.drawsContent) { + info.name = layer.objectInstance.name; + } else { + info.name = 'cc::LayerImpl'; + } + + if (layer.usingGpuRasterization) { + info.name += ' (G)'; + } + + info.isMaskLayer = isMask; + info.replicaLayer = isReplica; + + if (showPureTransformLayers || layer.args.drawsContent) { + layerInfos.push(info); + } + } + tree.iterLayers(visitLayer); + return layerInfos; + }, + + updateContents_() { + if (this.renderPassQuads_) { + this.updateRenderPassContents_(); + } else { + this.updateLayerContents_(); + } + }, + + updateRenderPassContents_() { + this.itemList_.clear(); + + let selectedRenderPassId; + if (this.selection_ && this.selection_.associatedRenderPassId) { + selectedRenderPassId = this.selection_.associatedRenderPassId; + } + + const renderPassInfos = this.getRenderPassInfos_(); + renderPassInfos.forEach(function(renderPassInfo) { + const renderPass = renderPassInfo.renderPass; + const id = renderPassInfo.id; + + const item = this.createElementWithDepth_(renderPassInfo.depth); + const labelEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan()); + + Polymer.dom(labelEl).textContent = renderPassInfo.name + ' ' + id; + item.renderPass = renderPass; + item.renderPassId = id; + Polymer.dom(this.itemList_).appendChild(item); + + if (id === selectedRenderPassId) { + renderPass.selectionState = + tr.model.SelectionState.SELECTED; + } + }, this); + }, + + updateLayerContents_() { + this.changingItemSelection_ = true; + try { + this.itemList_.clear(); + + let selectedLayerId; + if (this.selection_ && this.selection_.associatedLayerId) { + selectedLayerId = this.selection_.associatedLayerId; + } + + const layerInfos = this.getLayerInfos_(); + layerInfos.forEach(function(layerInfo) { + const layer = layerInfo.layer; + const id = layer.layerId; + + const item = this.createElementWithDepth_(layerInfo.depth); + const labelEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan()); + + Polymer.dom(labelEl).textContent = layerInfo.name + ' ' + id; + + const notesEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan()); + if (layerInfo.isMaskLayer) { + Polymer.dom(notesEl).textContent += '(mask)'; + } + if (layerInfo.isReplicaLayer) { + Polymer.dom(notesEl).textContent += '(replica)'; + } + + if ((layer.gpuMemoryUsageInBytes !== undefined) && + (layer.gpuMemoryUsageInBytes > 0)) { + const gpuUsageStr = tr.b.Unit.byName.sizeInBytes.format( + layer.gpuMemoryUsageInBytes); + Polymer.dom(notesEl).textContent += ' (' + gpuUsageStr + ' MiB)'; + } + + item.layer = layer; + Polymer.dom(this.itemList_).appendChild(item); + + if (layer.layerId === selectedLayerId) { + layer.selectionState = tr.model.SelectionState.SELECTED; + item.selected = true; + } + }, this); + } finally { + this.changingItemSelection_ = false; + } + }, + + createElementWithDepth_(depth) { + const item = document.createElement('div'); + + const indentEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan()); + indentEl.style.whiteSpace = 'pre'; + for (let i = 0; i < depth; i++) { + Polymer.dom(indentEl).textContent = + Polymer.dom(indentEl).textContent + ' '; + } + return item; + }, + + onItemSelectionChanged_(e) { + if (this.changingItemSelection_) return; + if (this.renderPassQuads_) { + this.onRenderPassSelected_(e); + } else { + this.onLayerSelected_(e); + } + tr.b.dispatchSimpleEvent(this, 'selection-change', false); + }, + + onRenderPassSelected_(e) { + let selectedRenderPass; + let selectedRenderPassId; + if (this.itemList_.selectedElement) { + selectedRenderPass = this.itemList_.selectedElement.renderPass; + selectedRenderPassId = + this.itemList_.selectedElement.renderPassId; + } + + if (selectedRenderPass) { + this.selection_ = new tr.ui.e.chrome.cc.RenderPassSelection( + selectedRenderPass, selectedRenderPassId); + } else { + this.selection_ = undefined; + } + }, + + onLayerSelected_(e) { + let selectedLayer; + if (this.itemList_.selectedElement) { + selectedLayer = this.itemList_.selectedElement.layer; + } + + if (selectedLayer) { + this.selection_ = new tr.ui.e.chrome.cc.LayerSelection(selectedLayer); + } else { + this.selection_ = undefined; + } + }, + + get selection() { + return this.selection_; + }, + + set selection(selection) { + if (this.selection_ === selection) return; + this.selection_ = selection; + this.updateContents_(); + } + }; + + return { + LayerPicker, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html new file mode 100644 index 00000000000..1aaee9d7fbc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html @@ -0,0 +1,142 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html"> +<link rel="import" href="/tracing/extras/chrome/cc/tile.html"> +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_picker.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + /* + * Displays a LayerTreeHostImpl snapshot in a human readable form. + * @constructor + */ + const LayerTreeHostImplSnapshotView = tr.ui.b.define( + 'tr-ui-e-chrome-cc-layer-tree-host-impl-snapshot-view', + tr.ui.analysis.ObjectSnapshotView); + + LayerTreeHostImplSnapshotView.prototype = { + __proto__: tr.ui.analysis.ObjectSnapshotView.prototype, + + decorate() { + Polymer.dom(this).classList.add('tr-ui-e-chrome-cc-lthi-s-view'); + this.style.display = 'flex'; + this.style.flexDirection = 'row'; + this.style.flexGrow = 1; + this.style.flexShrink = 1; + this.style.flexBasis = 'auto'; + this.style.minWidth = 0; + + this.selection_ = undefined; + + this.layerPicker_ = new tr.ui.e.chrome.cc.LayerPicker(); + this.layerPicker_.style.flexGrow = 0; + this.layerPicker_.style.flexShrink = 0; + this.layerPicker_.style.flexBasis = 'auto'; + this.layerPicker_.style.minWidth = '200px'; + this.layerPicker_.addEventListener( + 'selection-change', + this.onLayerPickerSelectionChanged_.bind(this)); + + this.layerView_ = new tr.ui.e.chrome.cc.LayerView(); + this.layerView_.addEventListener( + 'selection-change', + this.onLayerViewSelectionChanged_.bind(this)); + this.layerView_.style.flexGrow = 1; + this.layerView_.style.flexShrink = 1; + this.layerView_.style.flexBasis = 'auto'; + this.layerView_.style.minWidth = 0; + + this.dragHandle_ = document.createElement('tr-ui-b-drag-handle'); + this.dragHandle_.style.flexGrow = 0; + this.dragHandle_.style.flexShrink = 0; + this.dragHandle_.style.flexBasis = 'auto'; + this.dragHandle_.horizontal = false; + this.dragHandle_.target = this.layerPicker_; + + Polymer.dom(this).appendChild(this.layerPicker_); + Polymer.dom(this).appendChild(this.dragHandle_); + Polymer.dom(this).appendChild(this.layerView_); + + // Make sure we have the current values from layerView_ and layerPicker_, + // since those might have been created before we added the listener. + this.onLayerViewSelectionChanged_(); + this.onLayerPickerSelectionChanged_(); + }, + + get objectSnapshot() { + return this.objectSnapshot_; + }, + + set objectSnapshot(objectSnapshot) { + this.objectSnapshot_ = objectSnapshot; + + const lthi = this.objectSnapshot; + let layerTreeImpl; + if (lthi) { + layerTreeImpl = lthi.getTree(this.layerPicker_.whichTree); + } + + this.layerPicker_.lthiSnapshot = lthi; + this.layerView_.layerTreeImpl = layerTreeImpl; + this.layerView_.regenerateContent(); + + if (!this.selection_) return; + + this.selection = this.selection_.findEquivalent(lthi); + }, + + get selection() { + return this.selection_; + }, + + set selection(selection) { + if (this.selection_ === selection) return; + + this.selection_ = selection; + this.layerPicker_.selection = selection; + this.layerView_.selection = selection; + tr.b.dispatchSimpleEvent(this, 'cc-selection-change'); + }, + + onLayerPickerSelectionChanged_() { + this.selection_ = this.layerPicker_.selection; + this.layerView_.selection = this.selection; + this.layerView_.layerTreeImpl = this.layerPicker_.layerTreeImpl; + this.layerView_.isRenderPassQuads = this.layerPicker_.isRenderPassQuads; + this.layerView_.regenerateContent(); + tr.b.dispatchSimpleEvent(this, 'cc-selection-change'); + }, + + onLayerViewSelectionChanged_() { + this.selection_ = this.layerView_.selection; + this.layerPicker_.selection = this.selection; + tr.b.dispatchSimpleEvent(this, 'cc-selection-change'); + }, + + get extraHighlightsByLayerId() { + return this.layerView_.extraHighlightsByLayerId; + }, + + set extraHighlightsByLayerId(extraHighlightsByLayerId) { + this.layerView_.extraHighlightsByLayerId = extraHighlightsByLayerId; + } + }; + + tr.ui.analysis.ObjectSnapshotView.register( + LayerTreeHostImplSnapshotView, {typeName: 'cc::LayerTreeHostImpl'}); + + return { + LayerTreeHostImplSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html new file mode 100644 index 00000000000..1831be24618 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html"> +<link rel="import" href="/tracing/extras/chrome/cc/raster_task.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html"> + +<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js"> +</script> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = Object.values(m.processes)[0]; + + const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0]; + const snapshot = instance.snapshots[0]; + + const view = new tr.ui.e.chrome.cc.LayerTreeHostImplSnapshotView(); + view.style.width = '900px'; + view.style.height = '400px'; + view.objectSnapshot = snapshot; + + this.addHTMLOutput(view); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html new file mode 100644 index 00000000000..2a7e5666f8b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html @@ -0,0 +1,1200 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color.html"> +<link rel="import" href="/tracing/base/math/quad.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/base/unit_scale.html"> +<link rel="import" href="/tracing/extras/chrome/cc/debug_colors.html"> +<link rel="import" href="/tracing/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/extras/chrome/cc/render_pass.html"> +<link rel="import" href="/tracing/extras/chrome/cc/tile.html"> +<link rel="import" href="/tracing/extras/chrome/cc/util.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/base/info_bar.html"> +<link rel="import" href="/tracing/ui/base/quad_stack_view.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> + +<template id='tr-ui-e-chrome-cc-layer-tree-quad-stack-view-template'> + <style> + #input-event { + background-image: url('./images/input-event.png'); + display: none; + } + </style> + <img id='input-event'/> +</template> + +<script> +'use strict'; + +/** + * @fileoverview Graphical view of LayerTreeImpl, with controls for + * type of layer content shown and info bar for content-loading warnings. + */ +tr.exportTo('tr.ui.e.chrome.cc', function() { + const ColorScheme = tr.b.ColorScheme; + + const THIS_DOC = document.currentScript.ownerDocument; + const TILE_HEATMAP_TYPE = {}; + TILE_HEATMAP_TYPE.NONE = 'none'; + TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY = 'scheduledPriority'; + TILE_HEATMAP_TYPE.USING_GPU_MEMORY = 'usingGpuMemory'; + + const cc = tr.ui.e.chrome.cc; + + function createTileRectsSelectorBaseOptions() { + return [{label: 'None', value: 'none'}, + {label: 'Coverage Rects', value: 'coverage'}]; + } + + + /** + * @constructor + */ + const LayerTreeQuadStackView = + tr.ui.b.define('tr-ui-e-chrome-cc-layer-tree-quad-stack-view'); + + LayerTreeQuadStackView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.style.flexGrow = 1; + this.style.flexShrink = 1; + this.style.flexBasis = 'auto'; + this.style.flexDirection = 'column'; + this.style.minHeight = 0; + this.style.display = 'flex'; + + this.isRenderPassQuads_ = false; + this.pictureAsImageData_ = {}; // Maps picture.guid to PictureAsImageData. + this.messages_ = []; + this.controls_ = document.createElement('top-controls'); + this.controls_.style.flexGrow = 0; + this.controls_.style.flexShrink = 0; + this.controls_.style.flexBasis = 'auto'; + this.controls_.style.backgroundImage = + '-webkit-gradient(linear, 0 0, 100% 0, from(#E5E5E5), to(#D1D1D1))'; + this.controls_.style.borderBottom = '1px solid #8e8e8e'; + this.controls_.style.borderTop = '1px solid white'; + this.controls_.style.display = 'flex'; + this.controls_.style.flexDirection = 'row'; + this.controls_.style.flexWrap = 'wrap'; + this.controls_.style.fontSize = '14px'; + this.controls_.style.paddingLeft = '2px'; + this.controls_.style.overflow = 'hidden'; + this.infoBar_ = document.createElement('tr-ui-b-info-bar'); + this.quadStackView_ = new tr.ui.b.QuadStackView(); + this.quadStackView_.addEventListener( + 'selectionchange', this.onQuadStackViewSelectionChange_.bind(this)); + this.quadStackView_.style.flexGrow = 1; + this.quadStackView_.style.flexShrink = 1; + this.quadStackView_.style.flexBasis = 'auto'; + this.quadStackView_.style.minWidth = '200px'; + + this.extraHighlightsByLayerId_ = undefined; + this.inputEventImageData_ = undefined; + + const m = tr.ui.b.MOUSE_SELECTOR_MODE; + const mms = this.quadStackView_.mouseModeSelector; + mms.settingsKey = 'tr.e.cc.layerTreeQuadStackView.mouseModeSelector'; + mms.setKeyCodeForMode(m.SELECTION, 'Z'.charCodeAt(0)); + mms.setKeyCodeForMode(m.PANSCAN, 'X'.charCodeAt(0)); + mms.setKeyCodeForMode(m.ZOOM, 'C'.charCodeAt(0)); + mms.setKeyCodeForMode(m.ROTATE, 'V'.charCodeAt(0)); + + const node = tr.ui.b.instantiateTemplate( + '#tr-ui-e-chrome-cc-layer-tree-quad-stack-view-template', THIS_DOC); + Polymer.dom(this).appendChild(node); + Polymer.dom(this).appendChild(this.controls_); + Polymer.dom(this).appendChild(this.infoBar_); + Polymer.dom(this).appendChild(this.quadStackView_); + + this.tileRectsSelector_ = tr.ui.b.createSelector( + this, 'howToShowTiles', + 'layerView.howToShowTiles', 'none', + createTileRectsSelectorBaseOptions()); + Polymer.dom(this.controls_).appendChild(this.tileRectsSelector_); + + const tileHeatmapText = tr.ui.b.createSpan({ + textContent: 'Tile heatmap:' + }); + Polymer.dom(this.controls_).appendChild(tileHeatmapText); + + const tileHeatmapSelector = tr.ui.b.createSelector( + this, 'tileHeatmapType', + 'layerView.tileHeatmapType', TILE_HEATMAP_TYPE.NONE, + [{label: 'None', + value: TILE_HEATMAP_TYPE.NONE}, + {label: 'Scheduled Priority', + value: TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY}, + {label: 'Is using GPU memory', + value: TILE_HEATMAP_TYPE.USING_GPU_MEMORY} + ]); + Polymer.dom(this.controls_).appendChild(tileHeatmapSelector); + + const showOtherLayersCheckbox = tr.ui.b.createCheckBox( + this, 'showOtherLayers', + 'layerView.showOtherLayers', true, + 'Other layers/passes'); + showOtherLayersCheckbox.title = + 'When checked, show all layers, selected or not.'; + Polymer.dom(this.controls_).appendChild(showOtherLayersCheckbox); + + const showInvalidationsCheckbox = tr.ui.b.createCheckBox( + this, 'showInvalidations', + 'layerView.showInvalidations', true, + 'Invalidations'); + showInvalidationsCheckbox.title = + 'When checked, compositing invalidations are highlighted in red'; + Polymer.dom(this.controls_).appendChild(showInvalidationsCheckbox); + + const showUnrecordedRegionCheckbox = tr.ui.b.createCheckBox( + this, 'showUnrecordedRegion', + 'layerView.showUnrecordedRegion', true, + 'Unrecorded area'); + showUnrecordedRegionCheckbox.title = + 'When checked, unrecorded areas are highlighted in yellow'; + Polymer.dom(this.controls_).appendChild(showUnrecordedRegionCheckbox); + + const showBottlenecksCheckbox = tr.ui.b.createCheckBox( + this, 'showBottlenecks', + 'layerView.showBottlenecks', true, + 'Bottlenecks'); + showBottlenecksCheckbox.title = + 'When checked, scroll bottlenecks are highlighted'; + Polymer.dom(this.controls_).appendChild(showBottlenecksCheckbox); + + const showLayoutRectsCheckbox = tr.ui.b.createCheckBox( + this, 'showLayoutRects', + 'layerView.showLayoutRects', false, + 'Layout rects'); + showLayoutRectsCheckbox.title = + 'When checked, shows rects for regions where layout happened'; + Polymer.dom(this.controls_).appendChild(showLayoutRectsCheckbox); + + const showContentsCheckbox = tr.ui.b.createCheckBox( + this, 'showContents', + 'layerView.showContents', true, + 'Contents'); + showContentsCheckbox.title = + 'When checked, show the rendered contents inside the layer outlines'; + Polymer.dom(this.controls_).appendChild(showContentsCheckbox); + + const showAnimationBoundsCheckbox = tr.ui.b.createCheckBox( + this, 'showAnimationBounds', + 'layerView.showAnimationBounds', false, + 'Animation Bounds'); + showAnimationBoundsCheckbox.title = 'When checked, show a border around' + + ' a layer showing the extent of its animation.'; + Polymer.dom(this.controls_).appendChild(showAnimationBoundsCheckbox); + + const showInputEventsCheckbox = tr.ui.b.createCheckBox( + this, 'showInputEvents', + 'layerView.showInputEvents', true, + 'Input events'); + showInputEventsCheckbox.title = 'When checked, input events are ' + + 'displayed as circles.'; + Polymer.dom(this.controls_).appendChild(showInputEventsCheckbox); + + this.whatRasterizedLink_ = document.createElement( + 'tr-ui-a-analysis-link'); + this.whatRasterizedLink_.style.position = 'absolute'; + this.whatRasterizedLink_.style.bottom = '15px'; + this.whatRasterizedLink_.style.left = '10px'; + this.whatRasterizedLink_.selection = + this.getWhatRasterizedEventSet_.bind(this); + Polymer.dom(this.quadStackView_).appendChild(this.whatRasterizedLink_); + }, + + get layerTreeImpl() { + return this.layerTreeImpl_; + }, + + set isRenderPassQuads(newValue) { + this.isRenderPassQuads_ = newValue; + }, + + set layerTreeImpl(layerTreeImpl) { + if (this.layerTreeImpl_ === layerTreeImpl) return; + + // FIXME(pdr): We may want to clear pictureAsImageData_ here to save + // memory at the cost of performance. Note that + // pictureAsImageData_ will be cleared when this is + // destructed, but this view might live for several + // layerTreeImpls. + this.layerTreeImpl_ = layerTreeImpl; + this.selection = undefined; + }, + + get extraHighlightsByLayerId() { + return this.extraHighlightsByLayerId_; + }, + + set extraHighlightsByLayerId(extraHighlightsByLayerId) { + this.extraHighlightsByLayerId_ = extraHighlightsByLayerId; + this.scheduleUpdateContents_(); + }, + + get showOtherLayers() { + return this.showOtherLayers_; + }, + + set showOtherLayers(show) { + this.showOtherLayers_ = show; + this.updateContents_(); + }, + + get showAnimationBounds() { + return this.showAnimationBounds_; + }, + + set showAnimationBounds(show) { + this.showAnimationBounds_ = show; + this.updateContents_(); + }, + + get showInputEvents() { + return this.showInputEvents_; + }, + + set showInputEvents(show) { + this.showInputEvents_ = show; + this.updateContents_(); + }, + + get showContents() { + return this.showContents_; + }, + + set showContents(show) { + this.showContents_ = show; + this.updateContents_(); + }, + + get showInvalidations() { + return this.showInvalidations_; + }, + + set showInvalidations(show) { + this.showInvalidations_ = show; + this.updateContents_(); + }, + + get showUnrecordedRegion() { + return this.showUnrecordedRegion_; + }, + + set showUnrecordedRegion(show) { + this.showUnrecordedRegion_ = show; + this.updateContents_(); + }, + + get showBottlenecks() { + return this.showBottlenecks_; + }, + + set showBottlenecks(show) { + this.showBottlenecks_ = show; + this.updateContents_(); + }, + + get showLayoutRects() { + return this.showLayoutRects_; + }, + + set showLayoutRects(show) { + this.showLayoutRects_ = show; + this.updateContents_(); + }, + + get howToShowTiles() { + return this.howToShowTiles_; + }, + + set howToShowTiles(val) { + // Make sure val is something we expect. + if (val !== 'none' && val !== 'coverage' && isNaN(parseFloat(val))) { + throw new Error( + 'howToShowTiles requires "none" or "coverage" or a number'); + } + + this.howToShowTiles_ = val; + this.updateContents_(); + }, + + get tileHeatmapType() { + return this.tileHeatmapType_; + }, + + set tileHeatmapType(val) { + this.tileHeatmapType_ = val; + this.updateContents_(); + }, + + get selection() { + return this.selection_; + }, + + set selection(selection) { + if (this.selection === selection) return; + + this.selection_ = selection; + tr.b.dispatchSimpleEvent(this, 'selection-change'); + this.updateContents_(); + }, + + regenerateContent() { + this.updateTilesSelector_(); + this.updateContents_(); + }, + + loadDataForImageElement_(image, callback) { + const imageContent = window.getComputedStyle(image).backgroundImage; + if (!imageContent) { + // The style has not been applied because the view has not been added + // into the DOM tree yet. Try again in another cycle. + this.scheduleUpdateContents_(); + return; + } + image.src = tr.ui.b.extractUrlString(imageContent); + image.onload = function() { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = image.width; + canvas.height = image.height; + ctx.drawImage(image, 0, 0); + const imageData = ctx.getImageData( + 0, 0, canvas.width, canvas.height); + callback(imageData); + }; + }, + + onQuadStackViewSelectionChange_(e) { + const selectableQuads = e.quads.filter(function(q) { + return q.selectionToSetIfClicked !== undefined; + }); + if (selectableQuads.length === 0) { + this.selection = undefined; + return; + } + + // Sort the quads low to high on stackingGroupId. + selectableQuads.sort(function(x, y) { + const z = x.stackingGroupId - y.stackingGroupId; + if (z !== 0) return z; + + return x.selectionToSetIfClicked.specicifity - + y.selectionToSetIfClicked.specicifity; + }); + + // TODO(nduca): Support selecting N things at once. + const quadToSelect = selectableQuads[selectableQuads.length - 1]; + this.selection = quadToSelect.selectionToSetIfClicked; + }, + + scheduleUpdateContents_() { + if (this.updateContentsPending_) return; + + this.updateContentsPending_ = true; + tr.b.requestAnimationFrameInThisFrameIfPossible( + this.updateContents_, this); + }, + + updateContents_() { + if (!this.layerTreeImpl_) { + this.quadStackView_.headerText = 'No tree'; + this.quadStackView_.quads = []; + return; + } + + + const status = this.computePictureLoadingStatus_(); + if (!status.picturesComplete) return; + + const lthi = this.layerTreeImpl_.layerTreeHostImpl; + const lthiInstance = lthi.objectInstance; + const worldViewportRect = tr.b.math.Rect.fromXYWH( + 0, 0, + lthi.deviceViewportSize.width, lthi.deviceViewportSize.height); + this.quadStackView_.deviceRect = worldViewportRect; + if (this.isRenderPassQuads_) { + this.quadStackView_.quads = this.generateRenderPassQuads(); + } else { + this.quadStackView_.quads = this.generateLayerQuads(); + } + + this.updateWhatRasterizedLinkState_(); + + let message = ''; + if (lthi.tilesHaveGpuMemoryUsageInfo) { + const thisTreeUsageInBytes = this.layerTreeImpl_.gpuMemoryUsageInBytes; + const otherTreeUsageInBytes = lthi.gpuMemoryUsageInBytes - + thisTreeUsageInBytes; + message += + tr.b.convertUnit(thisTreeUsageInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + + ' MiB on this tree'; + if (otherTreeUsageInBytes) { + message += ', ' + + tr.b.convertUnit(otherTreeUsageInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + + ' MiB on the other tree'; + } + } else { + if (this.layerTreeImpl_) { + const thisTreeUsageInBytes = + this.layerTreeImpl_.gpuMemoryUsageInBytes; + message += + tr.b.convertUnit(thisTreeUsageInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + + ' MiB on this tree'; + + if (this.layerTreeImpl_.otherTree) { + // Older Chromes don't report enough data to know how much memory is + // being used across both trees. We know the memory consumed by each + // tree, but there is resource sharing *between the trees* so we + // can't simply sum up the per-tree costs. We need either the total + // plus one tree, to guess the unique on the other tree, etc. Newer + // chromes report memory per tile, which allows LTHI to compute the + // total tile memory usage, letting us figure things out properly. + message += ', ??? MiB on other tree. '; + } + } + } + + if (lthi.args.tileManagerBasicState) { + const tmgs = lthi.args.tileManagerBasicState.globalState; + message += ' (softMax=' + + tr.b.convertUnit(tmgs.softMemoryLimitInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + + ' MiB, hardMax=' + + tr.b.convertUnit(tmgs.hardMemoryLimitInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + ' MiB, ' + + tmgs.memoryLimitPolicy + ')'; + } else { + // Old Chromes do not have a globalState on the LTHI dump. + // But they do issue a DidManage event wiht the globalstate. Find that + // event so that we show some global state. + const thread = lthi.snapshottedOnThread; + const didManageTilesSlices = thread.sliceGroup.slices.filter(s => { + if (s.category !== 'tr.e.cc') return false; + + if (s.title !== 'DidManage') return false; + + if (s.end > lthi.ts) return false; + + return true; + }); + didManageTilesSlices.sort(function(x, y) { + return x.end - y.end; + }); + if (didManageTilesSlices.length > 0) { + const newest = didManageTilesSlices[didManageTilesSlices.length - 1]; + const tmgs = newest.args.state.global_state; + message += ' (softMax=' + + tr.b.convertUnit(tmgs.softMemoryLimitInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + + ' MiB, hardMax=' + + tr.b.convertUnit(tmgs.hardMemoryLimitInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + ' MiB, ' + + tmgs.memoryLimitPolicy + ')'; + } + } + + if (this.layerTreeImpl_.otherTree) { + message += ' (Another tree exists)'; + } + + if (message.length) { + this.quadStackView_.headerText = message; + } else { + this.quadStackView_.headerText = undefined; + } + + this.updateInfoBar_(status.messages); + }, + + updateTilesSelector_() { + const data = createTileRectsSelectorBaseOptions(); + + if (this.layerTreeImpl_) { + // First get all of the scales information from LTHI. + const lthi = this.layerTreeImpl_.layerTreeHostImpl; + const scaleNames = lthi.getContentsScaleNames(); + for (const scale in scaleNames) { + data.push({ + label: 'Scale ' + scale + ' (' + scaleNames[scale] + ')', + value: scale + }); + } + } + + // Then create a new selector and replace the old one. + const newSelector = tr.ui.b.createSelector( + this, 'howToShowTiles', + 'layerView.howToShowTiles', 'none', + data); + this.controls_.replaceChild(newSelector, this.tileRectsSelector_); + this.tileRectsSelector_ = newSelector; + }, + + computePictureLoadingStatus_() { + // Figure out if we can draw the quads yet. While we're at it, figure out + // if we have any warnings we need to show. + const layers = this.layers; + const status = { + messages: [], + picturesComplete: true + }; + if (this.showContents) { + let hasPendingRasterizeImage = false; + let firstPictureError = undefined; + let hasMissingLayerRect = false; + let hasUnresolvedPictureRef = false; + for (let i = 0; i < layers.length; i++) { + const layer = layers[i]; + for (let ir = 0; ir < layer.pictures.length; ++ir) { + const picture = layer.pictures[ir]; + + if (picture.idRef) { + hasUnresolvedPictureRef = true; + continue; + } + if (!picture.layerRect) { + hasMissingLayerRect = true; + continue; + } + + const pictureAsImageData = this.pictureAsImageData_[picture.guid]; + if (!pictureAsImageData) { + hasPendingRasterizeImage = true; + this.pictureAsImageData_[picture.guid] = + tr.e.cc.PictureAsImageData.Pending(this); + picture.rasterize( + {stopIndex: undefined}, + function(pictureImageData) { + const picture_ = pictureImageData.picture; + this.pictureAsImageData_[picture_.guid] = pictureImageData; + this.scheduleUpdateContents_(); + }.bind(this)); + continue; + } + if (pictureAsImageData.isPending()) { + hasPendingRasterizeImage = true; + continue; + } + if (pictureAsImageData.error) { + if (!firstPictureError) { + firstPictureError = pictureAsImageData.error; + } + break; + } + } + } + if (hasPendingRasterizeImage) { + status.picturesComplete = false; + } else { + if (hasUnresolvedPictureRef) { + status.messages.push({ + header: 'Missing picture', + details: 'Your trace didn\'t have pictures for every layer. ' + + 'Old chrome versions had this problem'}); + } + if (hasMissingLayerRect) { + status.messages.push({ + header: 'Missing layer rect', + details: 'Your trace may be corrupt or from a very old ' + + 'Chrome revision.'}); + } + if (firstPictureError) { + status.messages.push({ + header: 'Cannot rasterize', + details: firstPictureError}); + } + } + } + if (this.showInputEvents && this.layerTreeImpl.tracedInputLatencies && + this.inputEventImageData_ === undefined) { + const image = Polymer.dom(this).querySelector('#input-event'); + if (!image.src) { + this.loadDataForImageElement_(image, function(imageData) { + this.inputEventImageData_ = imageData; + this.updateContentsPending_ = false; + this.scheduleUpdateContents_(); + }.bind(this)); + } + status.picturesComplete = false; + } + return status; + }, + + get selectedRenderPass() { + if (this.selection) { + return this.selection.renderPass_; + } + }, + + get selectedLayer() { + if (this.selection) { + const selectedLayerId = this.selection.associatedLayerId; + return this.layerTreeImpl_.findLayerWithId(selectedLayerId); + } + }, + + get renderPasses() { + let renderPasses = + this.layerTreeImpl.layerTreeHostImpl.args.frame.renderPasses; + if (!this.showOtherLayers) { + const selectedRenderPass = this.selectedRenderPass; + if (selectedRenderPass) { + renderPasses = [selectedRenderPass]; + } + } + return renderPasses; + }, + + get layers() { + let layers = this.layerTreeImpl.renderSurfaceLayerList; + if (!this.showOtherLayers) { + const selectedLayer = this.selectedLayer; + if (selectedLayer) { + layers = [selectedLayer]; + } + } + return layers; + }, + + appendImageQuads_(quads, layer, layerQuad) { + // Generate image quads for the layer + for (let ir = 0; ir < layer.pictures.length; ++ir) { + const picture = layer.pictures[ir]; + if (!picture.layerRect) continue; + + const unitRect = picture.layerRect.asUVRectInside(layer.bounds); + const iq = layerQuad.projectUnitRect(unitRect); + + const pictureData = this.pictureAsImageData_[picture.guid]; + if (this.showContents && pictureData && pictureData.imageData) { + iq.imageData = pictureData.imageData; + iq.borderColor = 'rgba(0,0,0,0)'; + } else { + iq.imageData = undefined; + } + + iq.stackingGroupId = layerQuad.stackingGroupId; + quads.push(iq); + } + }, + + appendAnimationQuads_(quads, layer, layerQuad) { + if (!layer.animationBoundsRect) return; + + const rect = layer.animationBoundsRect; + const abq = tr.b.math.Quad.fromRect(rect); + + abq.backgroundColor = 'rgba(164,191,48,0.5)'; + abq.borderColor = 'rgba(205,255,0,0.75)'; + abq.borderWidth = 3.0; + abq.stackingGroupId = layerQuad.stackingGroupId; + abq.selectionToSetIfClicked = new cc.AnimationRectSelection( + layer, rect); + quads.push(abq); + }, + + appendInvalidationQuads_(quads, layer, layerQuad) { + if (layer.layerTreeImpl.hasSourceFrameBeenDrawnBefore) return; + + // Generate the invalidation rect quads. + for (const rect of layer.invalidation.rects) { + const unitRect = rect.asUVRectInside(layer.bounds); + const iq = layerQuad.projectUnitRect(unitRect); + iq.backgroundColor = 'rgba(0, 255, 0, 0.1)'; + if (rect.reason === 'appeared') { + iq.backgroundColor = 'rgba(0, 255, 128, 0.1)'; + } + iq.borderColor = 'rgba(0, 255, 0, 1)'; + iq.stackingGroupId = layerQuad.stackingGroupId; + + let message = 'Invalidation rect'; + if (rect.reason) { + message += ' (' + rect.reason + ')'; + } + if (rect.client) { + message += ' for ' + rect.client; + } + + iq.selectionToSetIfClicked = new cc.LayerRectSelection( + layer, message, rect, rect); + quads.push(iq); + } + }, + + appendUnrecordedRegionQuads_(quads, layer, layerQuad) { + // Generate the unrecorded region quads. + for (let ir = 0; ir < layer.unrecordedRegion.rects.length; ir++) { + const rect = layer.unrecordedRegion.rects[ir]; + const unitRect = rect.asUVRectInside(layer.bounds); + const iq = layerQuad.projectUnitRect(unitRect); + iq.backgroundColor = 'rgba(240, 230, 140, 0.3)'; + iq.borderColor = 'rgba(240, 230, 140, 1)'; + iq.stackingGroupId = layerQuad.stackingGroupId; + iq.selectionToSetIfClicked = new cc.LayerRectSelection( + layer, 'Unrecorded area', rect, rect); + quads.push(iq); + } + }, + + appendBottleneckQuads_(quads, layer, layerQuad, stackingGroupId) { + function processRegion(region, label, borderColor) { + const backgroundColor = borderColor.clone(); + backgroundColor.a = 0.4 * (borderColor.a || 1.0); + + if (!region || !region.rects) return; + + for (let ir = 0; ir < region.rects.length; ir++) { + const rect = region.rects[ir]; + const unitRect = rect.asUVRectInside(layer.bounds); + const iq = layerQuad.projectUnitRect(unitRect); + iq.backgroundColor = backgroundColor.toString(); + iq.borderColor = borderColor.toString(); + iq.borderWidth = 4.0; + iq.stackingGroupId = stackingGroupId; + iq.selectionToSetIfClicked = new cc.LayerRectSelection( + layer, label, rect, rect); + quads.push(iq); + } + } + + processRegion(layer.touchEventHandlerRegion, 'Touch listener', + tr.b.Color.fromString('rgb(228, 226, 27)')); + processRegion(layer.wheelEventHandlerRegion, 'Wheel listener', + tr.b.Color.fromString('rgb(176, 205, 29)')); + processRegion(layer.nonFastScrollableRegion, 'Repaints on scroll', + tr.b.Color.fromString('rgb(213, 134, 32)')); + }, + + appendTileCoverageRectQuads_( + quads, layer, layerQuad, heatmapType) { + if (!layer.tileCoverageRects) return; + + const tiles = []; + for (let ct = 0; ct < layer.tileCoverageRects.length; ++ct) { + const tile = layer.tileCoverageRects[ct].tile; + if (tile !== undefined) tiles.push(tile); + } + + const lthi = this.layerTreeImpl_.layerTreeHostImpl; + const minMax = + this.getMinMaxForHeatmap_(lthi.activeTiles, heatmapType); + const heatmapResult = + this.computeHeatmapColors_(tiles, minMax, heatmapType); + let heatIndex = 0; + + for (let ct = 0; ct < layer.tileCoverageRects.length; ++ct) { + let rect = layer.tileCoverageRects[ct].geometryRect; + rect = rect.scale(1.0 / layer.geometryContentsScale); + + const tile = layer.tileCoverageRects[ct].tile; + + const unitRect = rect.asUVRectInside(layer.bounds); + const quad = layerQuad.projectUnitRect(unitRect); + + quad.backgroundColor = 'rgba(0, 0, 0, 0)'; + quad.stackingGroupId = layerQuad.stackingGroupId; + let type = tr.e.cc.tileTypes.missing; + if (tile) { + type = tile.getTypeForLayer(layer); + quad.backgroundColor = heatmapResult[heatIndex].color; + ++heatIndex; + } + + quad.borderColor = tr.e.cc.tileBorder[type].color; + quad.borderWidth = tr.e.cc.tileBorder[type].width; + let label; + if (tile) { + label = 'coverageRect'; + } else { + label = 'checkerboard coverageRect'; + } + quad.selectionToSetIfClicked = new cc.LayerRectSelection( + layer, label, rect, layer.tileCoverageRects[ct]); + + quads.push(quad); + } + }, + + appendLayoutRectQuads_(quads, layer, layerQuad) { + if (!layer.layoutRects) { + return; + } + + for (let ct = 0; ct < layer.layoutRects.length; ++ct) { + let rect = layer.layoutRects[ct].geometryRect; + rect = rect.scale(1.0 / layer.geometryContentsScale); + + const unitRect = rect.asUVRectInside(layer.bounds); + const quad = layerQuad.projectUnitRect(unitRect); + + quad.backgroundColor = 'rgba(0, 0, 0, 0)'; + quad.stackingGroupId = layerQuad.stackingGroupId; + + quad.borderColor = 'rgba(0, 0, 200, 0.7)'; + quad.borderWidth = 2; + const label = 'Layout rect'; + quad.selectionToSetIfClicked = new cc.LayerRectSelection( + layer, label, rect); + + quads.push(quad); + } + }, + + getValueForHeatmap_(tile, heatmapType) { + if (heatmapType === TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY) { + return tile.scheduledPriority === 0 ? + undefined : + tile.scheduledPriority; + } else if (heatmapType === TILE_HEATMAP_TYPE.USING_GPU_MEMORY) { + if (tile.isSolidColor) return 0.5; + return tile.isUsingGpuMemory ? 0 : 1; + } + }, + + getMinMaxForHeatmap_(tiles, heatmapType) { + const range = new tr.b.math.Range(); + if (heatmapType === TILE_HEATMAP_TYPE.USING_GPU_MEMORY) { + range.addValue(0); + range.addValue(1); + return range; + } + + for (let i = 0; i < tiles.length; ++i) { + const value = this.getValueForHeatmap_(tiles[i], heatmapType); + if (value === undefined) continue; + range.addValue(value); + } + if (range.range === 0) { + range.addValue(1); + } + return range; + }, + + computeHeatmapColors_(tiles, minMax, heatmapType) { + const min = minMax.min; + const max = minMax.max; + + const color = function(value) { + let hue = 120 * (1 - (value - min) / (max - min)); + if (hue < 0) hue = 0; + return 'hsla(' + hue + ', 100%, 50%, 0.5)'; + }; + + const values = []; + for (let i = 0; i < tiles.length; ++i) { + const tile = tiles[i]; + const value = this.getValueForHeatmap_(tile, heatmapType); + const res = { + value, + color: value !== undefined ? color(value) : undefined + }; + values.push(res); + } + + return values; + }, + + appendTilesWithScaleQuads_( + quads, layer, layerQuad, scale, heatmapType) { + const lthi = this.layerTreeImpl_.layerTreeHostImpl; + + const tiles = []; + for (let i = 0; i < lthi.activeTiles.length; ++i) { + const tile = lthi.activeTiles[i]; + + if (Math.abs(tile.contentsScale - scale) > 1e-6) { + continue; + } + + // TODO(vmpstr): Make the stiching of tiles and layers a part of + // tile construction (issue 346) + if (layer.layerId !== tile.layerId) continue; + + tiles.push(tile); + } + + const minMax = + this.getMinMaxForHeatmap_(lthi.activeTiles, heatmapType); + const heatmapResult = + this.computeHeatmapColors_(tiles, minMax, heatmapType); + + for (let i = 0; i < tiles.length; ++i) { + const tile = tiles[i]; + const rect = tile.layerRect; + if (!tile.layerRect) continue; + + const unitRect = rect.asUVRectInside(layer.bounds); + const quad = layerQuad.projectUnitRect(unitRect); + + quad.backgroundColor = 'rgba(0, 0, 0, 0)'; + quad.stackingGroupId = layerQuad.stackingGroupId; + + const type = tile.getTypeForLayer(layer); + quad.borderColor = tr.e.cc.tileBorder[type].color; + quad.borderWidth = tr.e.cc.tileBorder[type].width; + + quad.backgroundColor = heatmapResult[i].color; + const data = { + tileType: type + }; + if (heatmapType !== TILE_HEATMAP_TYPE.NONE) { + data[heatmapType] = heatmapResult[i].value; + } + quad.selectionToSetIfClicked = new cc.TileSelection(tile, data); + quads.push(quad); + } + }, + + appendHighlightQuadsForLayer_( + quads, layer, layerQuad, highlights) { + highlights.forEach(function(highlight) { + const rect = highlight.rect; + + const unitRect = rect.asUVRectInside(layer.bounds); + const quad = layerQuad.projectUnitRect(unitRect); + + let colorId = ColorScheme.getColorIdForGeneralPurposeString( + highlight.colorKey); + const offset = ColorScheme.properties.brightenedOffsets[0]; + colorId = ColorScheme.getVariantColorId(colorId, offset); + + const color = ColorScheme.colors[colorId]; + + const quadForDrawing = quad.clone(); + quadForDrawing.backgroundColor = color.withAlpha(0.5).toString(); + quadForDrawing.borderColor = color.withAlpha(1.0).darken().toString(); + quadForDrawing.stackingGroupId = layerQuad.stackingGroupId; + quads.push(quadForDrawing); + }, this); + }, + + generateRenderPassQuads() { + if (!this.layerTreeImpl.layerTreeHostImpl.args.frame) return []; + const renderPasses = this.renderPasses; + if (!renderPasses) return []; + + const quads = []; + for (let i = 0; i < renderPasses.length; ++i) { + const quadList = renderPasses[i].quadList; + for (let j = 0; j < quadList.length; ++j) { + const drawQuad = quadList[j]; + const quad = drawQuad.rectAsTargetSpaceQuad.clone(); + quad.borderColor = 'rgb(170, 204, 238)'; + quad.borderWidth = 2; + quad.stackingGroupId = i; + quads.push(quad); + } + } + return quads; + }, + + generateLayerQuads() { + this.updateContentsPending_ = false; + + // Generate the quads for the view. + const layers = this.layers; + const quads = []; + let nextStackingGroupId = 0; + const alreadyVisitedLayerIds = {}; + + + let selectionHighlightsByLayerId; + if (this.selection) { + selectionHighlightsByLayerId = this.selection.highlightsByLayerId; + } else { + selectionHighlightsByLayerId = {}; + } + + const extraHighlightsByLayerId = this.extraHighlightsByLayerId || {}; + + for (let i = 1; i <= layers.length; i++) { + // Generate quads back-to-front. + const layer = layers[layers.length - i]; + alreadyVisitedLayerIds[layer.layerId] = true; + if (layer.objectInstance.name === 'cc::NinePatchLayerImpl') { + continue; + } + + const layerQuad = layer.layerQuad.clone(); + if (layer.usingGpuRasterization) { + const pixelRatio = window.devicePixelRatio || 1; + layerQuad.borderWidth = 2.0 * pixelRatio; + layerQuad.borderColor = 'rgba(154,205,50,0.75)'; + } else { + layerQuad.borderColor = 'rgba(0,0,0,0.75)'; + } + layerQuad.stackingGroupId = nextStackingGroupId++; + layerQuad.selectionToSetIfClicked = new cc.LayerSelection(layer); + layerQuad.layer = layer; + if (this.showOtherLayers && this.selectedLayer === layer) { + layerQuad.upperBorderColor = 'rgb(156,189,45)'; + } + + if (this.showAnimationBounds) { + this.appendAnimationQuads_(quads, layer, layerQuad); + } + + this.appendImageQuads_(quads, layer, layerQuad); + quads.push(layerQuad); + + + if (this.showInvalidations) { + this.appendInvalidationQuads_(quads, layer, layerQuad); + } + if (this.showUnrecordedRegion) { + this.appendUnrecordedRegionQuads_(quads, layer, layerQuad); + } + if (this.showBottlenecks) { + this.appendBottleneckQuads_(quads, layer, layerQuad, + layerQuad.stackingGroupId); + } + if (this.showLayoutRects) { + this.appendLayoutRectQuads_(quads, layer, layerQuad); + } + + if (this.howToShowTiles === 'coverage') { + this.appendTileCoverageRectQuads_( + quads, layer, layerQuad, this.tileHeatmapType); + } else if (this.howToShowTiles !== 'none') { + this.appendTilesWithScaleQuads_( + quads, layer, layerQuad, + this.howToShowTiles, this.tileHeatmapType); + } + + let highlights; + highlights = extraHighlightsByLayerId[layer.layerId]; + if (highlights) { + this.appendHighlightQuadsForLayer_( + quads, layer, layerQuad, highlights); + } + + highlights = selectionHighlightsByLayerId[layer.layerId]; + if (highlights) { + this.appendHighlightQuadsForLayer_( + quads, layer, layerQuad, highlights); + } + } + + this.layerTreeImpl.iterLayers(function(layer, depth, isMask, isReplica) { + if (!this.showOtherLayers && this.selectedLayer !== layer) return; + if (alreadyVisitedLayerIds[layer.layerId]) return; + + const layerQuad = layer.layerQuad; + const stackingGroupId = nextStackingGroupId++; + if (this.showBottlenecks) { + this.appendBottleneckQuads_(quads, layer, layerQuad, stackingGroupId); + } + }, this); + + const tracedInputLatencies = this.layerTreeImpl.tracedInputLatencies; + if (this.showInputEvents && tracedInputLatencies) { + for (let i = 0; i < tracedInputLatencies.length; i++) { + const coordinatesArray = + tracedInputLatencies[i].args.data.coordinates; + for (let j = 0; j < coordinatesArray.length; j++) { + const inputQuad = tr.b.math.Quad.fromXYWH( + coordinatesArray[j].x - 25, + coordinatesArray[j].y - 25, + 50, + 50); + inputQuad.borderColor = 'rgba(0, 0, 0, 0)'; + inputQuad.imageData = this.inputEventImageData_; + quads.push(inputQuad); + } + } + } + + return quads; + }, + + updateInfoBar_(infoBarMessages) { + if (infoBarMessages.length) { + this.infoBar_.removeAllButtons(); + this.infoBar_.message = 'Some problems were encountered...'; + this.infoBar_.addButton('More info...', function(e) { + const overlay = new tr.ui.b.Overlay(); + Polymer.dom(overlay).textContent = ''; + infoBarMessages.forEach(function(message) { + const title = document.createElement('h3'); + Polymer.dom(title).textContent = message.header; + + const details = document.createElement('div'); + Polymer.dom(details).textContent = message.details; + + Polymer.dom(overlay).appendChild(title); + Polymer.dom(overlay).appendChild(details); + }); + overlay.visible = true; + + e.stopPropagation(); + return false; + }); + this.infoBar_.visible = true; + } else { + this.infoBar_.removeAllButtons(); + this.infoBar_.message = ''; + this.infoBar_.visible = false; + } + }, + + getWhatRasterized_() { + const lthi = this.layerTreeImpl_.layerTreeHostImpl; + const renderProcess = lthi.objectInstance.parent; + const tasks = []; + for (const event of renderProcess.getDescendantEvents()) { + if (!(event instanceof tr.model.Slice)) continue; + + const tile = tr.e.cc.getTileFromRasterTaskSlice(event); + if (tile === undefined) continue; + + if (tile.containingSnapshot === lthi) { + tasks.push(event); + } + } + return tasks; + }, + + updateWhatRasterizedLinkState_() { + const tasks = this.getWhatRasterized_(); + if (tasks.length) { + Polymer.dom(this.whatRasterizedLink_).textContent = + tasks.length + ' raster tasks'; + this.whatRasterizedLink_.style.display = ''; + } else { + Polymer.dom(this.whatRasterizedLink_).textContent = ''; + this.whatRasterizedLink_.style.display = 'none'; + } + }, + + getWhatRasterizedEventSet_() { + return new tr.model.EventSet(this.getWhatRasterized_()); + } + }; + + return { + LayerTreeQuadStackView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html new file mode 100644 index 00000000000..66932ae785f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/cc.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html"> + +<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js"> +</script> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('tileCoverageRectCount', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + + const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0]; + const lthi = instance.snapshots[0]; + + const view = new tr.ui.e.chrome.cc.LayerTreeQuadStackView(); + view.layerTreeImpl = lthi.activeTree; + view.howToShowTiles = 'none'; + view.showInvalidations = false; + view.showContents = false; + + // There should be some quads drawn with all "show" checkboxes off, + // but that number can change with new features added. + const aQuads = view.generateLayerQuads(); + view.howToShowTiles = 'coverage'; + const bQuads = view.generateLayerQuads(); + const numCoverageRects = bQuads.length - aQuads.length; + + // We know we have 5 coverage rects in lthi cats. + assert.strictEqual(numCoverageRects, 5); + }); + + test('inputEvent', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + + const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0]; + const lthi = instance.snapshots[0]; + lthi.activeTree.tracedInputLatencies = + [{args: {data: {coordinates: [{x: 10, y: 20}, {x: 30, y: 40}]}}}]; + + const view = new tr.ui.e.chrome.cc.LayerTreeQuadStackView(); + view.layerTreeImpl = lthi.activeTree; + view.showInputEvents = false; + + const aQuads = view.generateLayerQuads(); + view.showInputEvents = true; + const bQuads = view.generateLayerQuads(); + const numInputEventRects = bQuads.length - aQuads.length; + + assert.strictEqual(numInputEventRects, 2); + + // We should not start loading the image until the view is added into the + // DOM tree. + const image = Polymer.dom(view).querySelector('#input-event'); + assert.strictEqual(getComputedStyle(image).backgroundImage, ''); + assert.strictEqual(image.src, ''); + + document.body.appendChild(view); + view.updateContents_(); + assert.notEqual(getComputedStyle(image).backgroundImage, ''); + assert.notEqual(image.src, ''); + view.remove(); + }); + + test('invalidation', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + + const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0]; + const lthi = instance.snapshots[0]; + + const view = new tr.ui.e.chrome.cc.LayerTreeQuadStackView(); + view.layerTreeImpl = lthi.activeTree; + view.showInvalidations = false; + + const aQuads = view.generateLayerQuads(); + view.showInvalidations = true; + const bQuads = view.generateLayerQuads(); + const numInvalidationRects = bQuads.length - aQuads.length; + + // We know we have 3 invalidation rects. + assert.strictEqual(numInvalidationRects, 3); + + const expectedRectTypes = [ + 'Invalidation rect (appeared) for client1', + 'Invalidation rect (disappeared) for client2', + 'Invalidation rect' // The non-annotated rect. + ]; + const found = []; + for (const quad of bQuads) { + const i = expectedRectTypes.indexOf(quad.selectionToSetIfClicked && + quad.selectionToSetIfClicked.rectType_); + if (i !== -1) { + found[i] = true; + } + } + assert.deepEqual(found, [true, true, true]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html new file mode 100644 index 00000000000..56ecf770ec1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html @@ -0,0 +1,165 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/extras/chrome/cc/constants.html"> +<link rel="import" href="/tracing/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html"> + +<script> +'use strict'; + +/** + * @fileoverview LayerView coordinates graphical and analysis views of layers. + */ + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const constants = tr.e.cc.constants; + + /** + * @constructor + */ + const LayerView = tr.ui.b.define('tr-ui-e-chrome-cc-layer-view'); + + LayerView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.style.flexDirection = 'column'; + this.style.display = 'flex'; + + this.layerTreeQuadStackView_ = + new tr.ui.e.chrome.cc.LayerTreeQuadStackView(); + this.dragBar_ = document.createElement('tr-ui-b-drag-handle'); + this.analysisEl_ = + document.createElement('tr-ui-e-chrome-cc-layer-view-analysis'); + this.analysisEl_.style.flexGrow = 0; + this.analysisEl_.style.flexShrink = 0; + this.analysisEl_.style.flexBasis = 'auto'; + this.analysisEl_.style.height = '150px'; + this.analysisEl_.style.overflow = 'auto'; + this.analysisEl_.addEventListener('requestSelectionChange', + this.onRequestSelectionChangeFromAnalysisEl_.bind(this)); + + this.dragBar_.target = this.analysisEl_; + + Polymer.dom(this).appendChild(this.layerTreeQuadStackView_); + Polymer.dom(this).appendChild(this.dragBar_); + Polymer.dom(this).appendChild(this.analysisEl_); + + this.layerTreeQuadStackView_.addEventListener('selection-change', + function() { + this.layerTreeQuadStackViewSelectionChanged_(); + }.bind(this)); + this.layerTreeQuadStackViewSelectionChanged_(); + }, + + get layerTreeImpl() { + return this.layerTreeQuadStackView_.layerTreeImpl; + }, + + set layerTreeImpl(newValue) { + return this.layerTreeQuadStackView_.layerTreeImpl = newValue; + }, + + set isRenderPassQuads(newValue) { + return this.layerTreeQuadStackView_.isRenderPassQuads = newValue; + }, + + get selection() { + return this.layerTreeQuadStackView_.selection; + }, + + set selection(newValue) { + this.layerTreeQuadStackView_.selection = newValue; + }, + + regenerateContent() { + this.layerTreeQuadStackView_.regenerateContent(); + }, + + layerTreeQuadStackViewSelectionChanged_() { + const selection = this.layerTreeQuadStackView_.selection; + if (selection) { + this.dragBar_.style.display = ''; + this.analysisEl_.style.display = ''; + Polymer.dom(this.analysisEl_).textContent = ''; + + const layer = selection.layer; + if (tr.e.cc.PictureSnapshot.CanDebugPicture() && + layer && + layer.args && + layer.args.pictures && + layer.args.pictures.length) { + Polymer.dom(this.analysisEl_).appendChild( + this.createPictureBtn_(layer.args.pictures)); + } + + const analysis = selection.createAnalysis(); + Polymer.dom(this.analysisEl_).appendChild(analysis); + for (const child of this.analysisEl_.children) { + child.style.userSelect = 'text'; + } + } else { + this.dragBar_.style.display = 'none'; + this.analysisEl_.style.display = 'none'; + const analysis = Polymer.dom(this.analysisEl_).firstChild; + if (analysis) { + Polymer.dom(this.analysisEl_).removeChild(analysis); + } + this.layerTreeQuadStackView_.style.height = + window.getComputedStyle(this).height; + } + tr.b.dispatchSimpleEvent(this, 'selection-change'); + }, + + createPictureBtn_(pictures) { + if (!(pictures instanceof Array)) { + pictures = [pictures]; + } + + const link = document.createElement('tr-ui-a-analysis-link'); + link.selection = function() { + const layeredPicture = new tr.e.cc.LayeredPicture(pictures); + const snapshot = new tr.e.cc.PictureSnapshot(layeredPicture); + snapshot.picture = layeredPicture; + + const selection = new tr.model.EventSet(); + selection.push(snapshot); + return selection; + }; + Polymer.dom(link).textContent = 'View in Picture Debugger'; + return link; + }, + + onRequestSelectionChangeFromAnalysisEl_(e) { + if (!(e.selection instanceof tr.ui.e.chrome.cc.Selection)) { + return; + } + + e.stopPropagation(); + this.selection = e.selection; + }, + + get extraHighlightsByLayerId() { + return this.layerTreeQuadStackView_.extraHighlightsByLayerId; + }, + + set extraHighlightsByLayerId(extraHighlightsByLayerId) { + this.layerTreeQuadStackView_.extraHighlightsByLayerId = + extraHighlightsByLayerId; + } + }; + + return { + LayerView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html new file mode 100644 index 00000000000..ed3de7b87e1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/cc.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_view.html"> + +<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js"> +</script> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + + const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0]; + const lthi = instance.snapshots[0]; + const numLayers = lthi.activeTree.renderSurfaceLayerList.length; + const layer = lthi.activeTree.renderSurfaceLayerList[numLayers - 1]; + + const view = new tr.ui.e.chrome.cc.LayerView(); + view.style.height = '500px'; + view.layerTreeImpl = lthi.activeTree; + view.selection = new tr.ui.e.chrome.cc.LayerSelection(layer); + + this.addHTMLOutput(view); + }); + + test('instantiate_withTileHighlight', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + + const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0]; + const lthi = instance.snapshots[0]; + const numLayers = lthi.activeTree.renderSurfaceLayerList.length; + const layer = lthi.activeTree.renderSurfaceLayerList[numLayers - 1]; + const tile = lthi.activeTiles[0]; + + const view = new tr.ui.e.chrome.cc.LayerView(); + view.style.height = '500px'; + view.layerTreeImpl = lthi.activeTree; + view.selection = new tr.ui.e.chrome.cc.TileSelection(tile); + this.addHTMLOutput(view); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html new file mode 100644 index 00000000000..5dc62b1b4f6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html @@ -0,0 +1,455 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base64.html"> +<link rel="import" href="/tracing/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> +<link rel="import" href="/tracing/ui/base/info_bar.html"> +<link rel="import" href="/tracing/ui/base/list_view.html"> +<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html"> +<link rel="import" href="/tracing/ui/base/overlay.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> +<link rel="import" + href="/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_list_view.html"> + +<template id="tr-ui-e-chrome-cc-picture-debugger-template"> + <left-panel> + <picture-info> + <div> + <span class='title'>Skia Picture</span> + <span class='size'></span> + </div> + <div> + <input class='filename' type='text' value='skpicture.skp' /> + <button class='export'>Export</button> + </div> + </picture-info> + </left-panel> + <right-panel> + <tr-ui-e-chrome-cc-picture-ops-chart-view> + </tr-ui-e-chrome-cc-picture-ops-chart-view> + <raster-area><canvas></canvas></raster-area> + </right-panel> +</template> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const THIS_DOC = document.currentScript.ownerDocument; + + /** + * PictureDebugger is a view of a PictureSnapshot for inspecting + * the picture in detail. (e.g., timing information, etc.) + * + * @constructor + */ + const PictureDebugger = tr.ui.b.define('tr-ui-e-chrome-cc-picture-debugger'); + + PictureDebugger.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + const node = tr.ui.b.instantiateTemplate( + '#tr-ui-e-chrome-cc-picture-debugger-template', THIS_DOC); + + Polymer.dom(this).appendChild(node); + + this.style.display = 'flex'; + this.style.flexDirection = 'row'; + + const title = this.querySelector('.title'); + title.style.fontWeight = 'bold'; + title.style.marginLeft = '5px'; + title.style.marginRight = '5px'; + + this.pictureAsImageData_ = undefined; + this.showOverdraw_ = false; + this.zoomScaleValue_ = 1; + + this.sizeInfo_ = Polymer.dom(this).querySelector('.size'); + this.rasterArea_ = Polymer.dom(this).querySelector('raster-area'); + this.rasterArea_.style.backgroundColor = '#ddd'; + this.rasterArea_.style.minHeight = '100px'; + this.rasterArea_.style.minWidth = '200px'; + this.rasterArea_.style.overflow = 'auto'; + this.rasterArea_.style.paddingLeft = '5px'; + this.rasterCanvas_ = Polymer.dom(this.rasterArea_) + .querySelector('canvas'); + this.rasterCtx_ = this.rasterCanvas_.getContext('2d'); + + this.filename_ = Polymer.dom(this).querySelector('.filename'); + this.filename_.style.userSelect = 'text'; + this.filename_.style.marginLeft = '5px'; + + this.drawOpsChartSummaryView_ = + new tr.ui.e.chrome.cc.PictureOpsChartSummaryView(); + this.drawOpsChartView_ = new tr.ui.e.chrome.cc.PictureOpsChartView(); + this.drawOpsChartView_.addEventListener( + 'selection-changed', this.onChartBarClicked_.bind(this)); + + this.exportButton_ = Polymer.dom(this).querySelector('.export'); + this.exportButton_.addEventListener( + 'click', this.onSaveAsSkPictureClicked_.bind(this)); + + this.trackMouse_(); + + const overdrawCheckbox = tr.ui.b.createCheckBox( + this, 'showOverdraw', + 'pictureView.showOverdraw', false, + 'Show overdraw'); + + const chartCheckbox = tr.ui.b.createCheckBox( + this, 'showSummaryChart', + 'pictureView.showSummaryChart', false, + 'Show timing summary'); + + const pictureInfo = Polymer.dom(this).querySelector('picture-info'); + pictureInfo.style.flexGrow = 0; + pictureInfo.style.flexShrink = 0; + pictureInfo.style.flexBasis = 'auto'; + pictureInfo.style.paddingTop = '2px'; + Polymer.dom(pictureInfo).appendChild(overdrawCheckbox); + Polymer.dom(pictureInfo).appendChild(chartCheckbox); + + this.drawOpsView_ = new tr.ui.e.chrome.cc.PictureOpsListView(); + this.drawOpsView_.flexGrow = 1; + this.drawOpsView_.flexShrink = 1; + this.drawOpsView_.flexBasis = 'auto'; + this.drawOpsView_.addEventListener( + 'selection-changed', this.onChangeDrawOps_.bind(this)); + + const leftPanel = Polymer.dom(this).querySelector('left-panel'); + leftPanel.style.flexDirection = 'column'; + leftPanel.style.display = 'flex'; + leftPanel.style.flexGrow = 0; + leftPanel.style.flexShrink = 0; + leftPanel.style.flexBasis = 'auto'; + leftPanel.style.minWidth = '200px'; + leftPanel.style.overflow = 'auto'; + Polymer.dom(leftPanel).appendChild(this.drawOpsChartSummaryView_); + Polymer.dom(leftPanel).appendChild(this.drawOpsView_); + + const middleDragHandle = document.createElement('tr-ui-b-drag-handle'); + middleDragHandle.style.flexGrow = 0; + middleDragHandle.style.flexShrink = 0; + middleDragHandle.style.flexBasis = 'auto'; + middleDragHandle.horizontal = false; + middleDragHandle.target = leftPanel; + + const rightPanel = Polymer.dom(this).querySelector('right-panel'); + rightPanel.style.flexGrow = 1; + rightPanel.style.flexShrink = 1; + rightPanel.style.flexBasis = 'auto'; + rightPanel.style.minWidth = 0; + rightPanel.style.flexDirection = 'column'; + rightPanel.style.display = 'flex'; + + const chartView = Polymer.dom(rightPanel).querySelector( + 'tr-ui-e-chrome-cc-picture-ops-chart-view'); + this.drawOpsChartView_.style.flexGrow = 0; + this.drawOpsChartView_.style.flexShrink = 0; + this.drawOpsChartView_.style.flexBasis = 'auto'; + this.drawOpsChartView_.style.minWidth = 0; + this.drawOpsChartView_.style.overflowX = 'auto'; + this.drawOpsChartView_.style.overflowY = 'hidden'; + rightPanel.replaceChild(this.drawOpsChartView_, chartView); + + this.infoBar_ = document.createElement('tr-ui-b-info-bar'); + Polymer.dom(this.rasterArea_).appendChild(this.infoBar_); + + Polymer.dom(this).insertBefore(middleDragHandle, rightPanel); + + this.picture_ = undefined; + + const hkc = document.createElement('tv-ui-b-hotkey-controller'); + hkc.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keypress', + thisArg: this, + keyCode: 'h'.charCodeAt(0), + callback(e) { + this.moveSelectedOpBy(-1); + e.stopPropagation(); + } + })); + hkc.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keypress', + thisArg: this, + keyCode: 'l'.charCodeAt(0), + callback(e) { + this.moveSelectedOpBy(1); + e.stopPropagation(); + } + })); + Polymer.dom(this).appendChild(hkc); + }, + + onSaveAsSkPictureClicked_() { + // Decode base64 data into a String + const rawData = tr.b.Base64.atob(this.picture_.getBase64SkpData()); + + // Convert this String into an Uint8Array + const length = rawData.length; + const arrayBuffer = new ArrayBuffer(length); + const uint8Array = new Uint8Array(arrayBuffer); + for (let c = 0; c < length; c++) { + uint8Array[c] = rawData.charCodeAt(c); + } + + // Create a blob URL from the binary array. + const blob = new Blob([uint8Array], {type: 'application/octet-binary'}); + const blobUrl = window.webkitURL.createObjectURL(blob); + + // Create a link and click on it. BEST API EVAR! + const link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); + link.href = blobUrl; + link.download = this.filename_.value; + const event = document.createEvent('MouseEvents'); + event.initMouseEvent( + 'click', true, false, window, 0, 0, 0, 0, 0, + false, false, false, false, 0, null); + link.dispatchEvent(event); + }, + + get picture() { + return this.picture_; + }, + + set picture(picture) { + this.drawOpsView_.picture = picture; + this.drawOpsChartView_.picture = picture; + this.drawOpsChartSummaryView_.picture = picture; + this.picture_ = picture; + + this.exportButton_.disabled = !this.picture_.canSave; + + if (picture) { + const size = this.getRasterCanvasSize_(); + this.rasterCanvas_.width = size.width; + this.rasterCanvas_.height = size.height; + } + + const bounds = this.rasterArea_.getBoundingClientRect(); + const selectorBounds = this.mouseModeSelector_.getBoundingClientRect(); + this.mouseModeSelector_.pos = { + x: (bounds.right - selectorBounds.width - 10), + y: bounds.top + }; + + this.rasterize_(); + + this.scheduleUpdateContents_(); + }, + + getRasterCanvasSize_() { + const style = window.getComputedStyle(this.rasterArea_); + const width = + Math.max(parseInt(style.width), this.picture_.layerRect.width); + const height = + Math.max(parseInt(style.height), this.picture_.layerRect.height); + + return { + width, + height + }; + }, + + scheduleUpdateContents_() { + if (this.updateContentsPending_) return; + + this.updateContentsPending_ = true; + tr.b.requestAnimationFrameInThisFrameIfPossible( + this.updateContents_.bind(this) + ); + }, + + updateContents_() { + this.updateContentsPending_ = false; + + if (this.picture_) { + Polymer.dom(this.sizeInfo_).textContent = '(' + + this.picture_.layerRect.width + ' x ' + + this.picture_.layerRect.height + ')'; + } + + this.drawOpsChartView_.updateChartContents(); + this.drawOpsChartView_.scrollSelectedItemIntoViewIfNecessary(); + + // Return if picture hasn't finished rasterizing. + if (!this.pictureAsImageData_) return; + + this.infoBar_.visible = false; + this.infoBar_.removeAllButtons(); + if (this.pictureAsImageData_.error) { + this.infoBar_.message = 'Cannot rasterize...'; + this.infoBar_.addButton('More info...', function(e) { + const overlay = new tr.ui.b.Overlay(); + Polymer.dom(overlay).textContent = this.pictureAsImageData_.error; + overlay.visible = true; + e.stopPropagation(); + return false; + }.bind(this)); + this.infoBar_.visible = true; + } + + this.drawPicture_(); + }, + + drawPicture_() { + const size = this.getRasterCanvasSize_(); + if (size.width !== this.rasterCanvas_.width) { + this.rasterCanvas_.width = size.width; + } + if (size.height !== this.rasterCanvas_.height) { + this.rasterCanvas_.height = size.height; + } + + this.rasterCtx_.clearRect(0, 0, size.width, size.height); + + if (!this.pictureAsImageData_.imageData) return; + + const imgCanvas = this.pictureAsImageData_.asCanvas(); + const w = imgCanvas.width; + const h = imgCanvas.height; + this.rasterCtx_.drawImage(imgCanvas, 0, 0, w, h, + 0, 0, w * this.zoomScaleValue_, + h * this.zoomScaleValue_); + }, + + rasterize_() { + if (this.picture_) { + this.picture_.rasterize( + { + stopIndex: this.drawOpsView_.selectedOpIndex, + showOverdraw: this.showOverdraw_ + }, + this.onRasterComplete_.bind(this)); + } + }, + + onRasterComplete_(pictureAsImageData) { + this.pictureAsImageData_ = pictureAsImageData; + this.scheduleUpdateContents_(); + }, + + moveSelectedOpBy(increment) { + if (this.selectedOpIndex === undefined) { + this.selectedOpIndex = 0; + return; + } + this.selectedOpIndex = tr.b.math.clamp( + this.selectedOpIndex + increment, + 0, this.numOps); + }, + + get numOps() { + return this.drawOpsView_.numOps; + }, + + get selectedOpIndex() { + return this.drawOpsView_.selectedOpIndex; + }, + + set selectedOpIndex(index) { + this.drawOpsView_.selectedOpIndex = index; + this.drawOpsChartView_.selectedOpIndex = index; + }, + + onChartBarClicked_(e) { + this.drawOpsView_.selectedOpIndex = + this.drawOpsChartView_.selectedOpIndex; + }, + + onChangeDrawOps_(e) { + this.rasterize_(); + this.scheduleUpdateContents_(); + + this.drawOpsChartView_.selectedOpIndex = + this.drawOpsView_.selectedOpIndex; + }, + + set showOverdraw(v) { + this.showOverdraw_ = v; + this.rasterize_(); + }, + + set showSummaryChart(chartShouldBeVisible) { + if (chartShouldBeVisible) { + this.drawOpsChartSummaryView_.show(); + } else { + this.drawOpsChartSummaryView_.hide(); + } + }, + + trackMouse_() { + this.mouseModeSelector_ = document.createElement( + 'tr-ui-b-mouse-mode-selector'); + this.mouseModeSelector_.targetElement = this.rasterArea_; + Polymer.dom(this.rasterArea_).appendChild(this.mouseModeSelector_); + + this.mouseModeSelector_.supportedModeMask = + tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM; + this.mouseModeSelector_.mode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM; + this.mouseModeSelector_.defaultMode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM; + this.mouseModeSelector_.settingsKey = 'pictureDebugger.mouseModeSelector'; + + this.mouseModeSelector_.addEventListener('beginzoom', + this.onBeginZoom_.bind(this)); + this.mouseModeSelector_.addEventListener('updatezoom', + this.onUpdateZoom_.bind(this)); + this.mouseModeSelector_.addEventListener('endzoom', + this.onEndZoom_.bind(this)); + }, + + onBeginZoom_(e) { + this.isZooming_ = true; + + this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e); + + e.preventDefault(); + }, + + onUpdateZoom_(e) { + if (!this.isZooming_) return; + + const currentMouseViewPos = this.extractRelativeMousePosition_(e); + + // Take the distance the mouse has moved and we want to zoom at about + // 1/1000th of that speed. 0.01 feels jumpy. This could possibly be tuned + // more if people feel it's too slow. + this.zoomScaleValue_ += + ((this.lastMouseViewPos_.y - currentMouseViewPos.y) * 0.001); + this.zoomScaleValue_ = Math.max(this.zoomScaleValue_, 0.1); + + this.drawPicture_(); + + this.lastMouseViewPos_ = currentMouseViewPos; + }, + + onEndZoom_(e) { + this.lastMouseViewPos_ = undefined; + this.isZooming_ = false; + e.preventDefault(); + }, + + extractRelativeMousePosition_(e) { + return { + x: e.clientX - this.rasterArea_.offsetLeft, + y: e.clientY - this.rasterArea_.offsetTop + }; + } + }; + + return { + PictureDebugger, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html new file mode 100644 index 00000000000..e89e6b355e1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_debugger.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const picture = new tr.e.cc.PictureSnapshot({id: '31415'}, 10, { + 'params': { + 'opaque_rect': [-15, -15, 0, 0], + 'layer_rect': [-15, -15, 46, 833] + }, + 'skp64': 'DAAAAHYEAADzAQAABwAAAAFkYWVy8AAAAAgAAB4DAAAADAAAIAAAgD8AAIA/CAAAHgMAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADoAAAACAAAHgMAAAAMAAAjAAAAAAAAAAAMAAAjAAAAAAAAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADkAAAAGAAAFQEAAAAAAAAAAAAAAADAjkQAgPlDGAAAFQIAAAAAAAAAAAAAAADAjkQAgPlDCAAAHgMAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADgAAAAGAAAFQMAAAAAAKBAAACgQAAAgEIAAIBCBAAAHAQAABwEAAAcBAAAHHRjYWYBAAAADVNrU3JjWGZlcm1vZGVjZnB0AAAAAHlhcmGgAAAAIHRucAMAAAAAAEBBAACAPwAAAAAAAIA/AAAAAAAAgEAAAP//ADABAAAAAAAAAEBBAACAPwAAAAAAAIA/AAAAAAAAgED/////AjABAAAAAAAAAAAAAAAAAAEAAAAEAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEEAAIA/AAAAAAAAgD8AAAAAAACAQP8AAP8AMAEAAAAAACBmb2U=' // @suppress longLineCheck + }); + picture.preInitialize(); + picture.initialize(); + + const dbg = new tr.ui.e.chrome.cc.PictureDebugger(); + this.addHTMLOutput(dbg); + dbg.picture = picture; + dbg.style.border = '1px solid black'; + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html new file mode 100644 index 00000000000..55a8685aee0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html @@ -0,0 +1,458 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/ui.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const OPS_TIMING_ITERATIONS = 3; + const CHART_PADDING_LEFT = 65; + const CHART_PADDING_RIGHT = 40; + const AXIS_PADDING_LEFT = 60; + const AXIS_PADDING_RIGHT = 35; + const AXIS_PADDING_TOP = 25; + const AXIS_PADDING_BOTTOM = 45; + const AXIS_LABEL_PADDING = 5; + const AXIS_TICK_SIZE = 10; + const LABEL_PADDING = 5; + const LABEL_INTERLEAVE_OFFSET = 15; + const BAR_PADDING = 5; + const VERTICAL_TICKS = 5; + const HUE_CHAR_CODE_ADJUSTMENT = 5.7; + + /** + * Provides a chart showing the cumulative time spent in Skia operations + * during picture rasterization. + * + * @constructor + */ + const PictureOpsChartSummaryView = tr.ui.b.define( + 'tr-ui-e-chrome-cc-picture-ops-chart-summary-view'); + + PictureOpsChartSummaryView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.style.flexGrow = 0; + this.style.flexShrink = 0; + this.style.flexBasis = 'auto'; + this.style.fontSize = 0; + this.style.margin = 0; + this.style.minHeight = '200px'; + this.style.minWidth = '200px'; + this.style.overflow = 'hidden'; + this.style.padding = 0; + + this.picture_ = undefined; + this.pictureDataProcessed_ = false; + + this.chartScale_ = window.devicePixelRatio; + + this.chart_ = document.createElement('canvas'); + this.chartCtx_ = this.chart_.getContext('2d'); + Polymer.dom(this).appendChild(this.chart_); + + this.opsTimingData_ = []; + + this.chartWidth_ = 0; + this.chartHeight_ = 0; + this.requiresRedraw_ = true; + + this.currentBarMouseOverTarget_ = null; + + this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this)); + new ResizeObserver(this.onResize_.bind(this)).observe(this); + }, + + get requiresRedraw() { + return this.requiresRedraw_; + }, + + set requiresRedraw(requiresRedraw) { + this.requiresRedraw_ = requiresRedraw; + }, + + get picture() { + return this.picture_; + }, + + set picture(picture) { + this.picture_ = picture; + this.pictureDataProcessed_ = false; + + if (Polymer.dom(this).classList.contains('hidden')) return; + + this.processPictureData_(); + this.requiresRedraw = true; + this.updateChartContents(); + }, + + hide() { + Polymer.dom(this).classList.add('hidden'); + this.style.display = 'none'; + }, + + show() { + Polymer.dom(this).classList.remove('hidden'); + this.style.display = ''; + + if (!this.pictureDataProcessed_) { + this.processPictureData_(); + } + this.requiresRedraw = true; + this.updateChartContents(); + }, + + onMouseMove_(e) { + const lastBarMouseOverTarget = this.currentBarMouseOverTarget_; + this.currentBarMouseOverTarget_ = null; + + const x = e.offsetX; + const y = e.offsetY; + + const chartLeft = CHART_PADDING_LEFT; + const chartRight = this.chartWidth_ - CHART_PADDING_RIGHT; + const chartTop = AXIS_PADDING_TOP; + const chartBottom = this.chartHeight_ - AXIS_PADDING_BOTTOM; + const chartInnerWidth = chartRight - chartLeft; + + if (x > chartLeft && x < chartRight && y > chartTop && y < chartBottom) { + this.currentBarMouseOverTarget_ = Math.floor( + (x - chartLeft) / chartInnerWidth * this.opsTimingData_.length); + + this.currentBarMouseOverTarget_ = tr.b.math.clamp( + this.currentBarMouseOverTarget_, 0, this.opsTimingData_.length - 1); + } + + if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget) return; + + this.drawChartContents_(); + }, + + onResize_() { + this.requiresRedraw = true; + this.updateChartContents(); + }, + + updateChartContents() { + if (this.requiresRedraw) { + this.updateChartDimensions_(); + } + + this.drawChartContents_(); + }, + + updateChartDimensions_() { + this.chartWidth_ = this.offsetWidth; + this.chartHeight_ = this.offsetHeight; + + // Scale up the canvas according to the devicePixelRatio, then reduce it + // down again via CSS. Finally we apply a scale to the canvas so that + // things are drawn at the correct size. + this.chart_.width = this.chartWidth_ * this.chartScale_; + this.chart_.height = this.chartHeight_ * this.chartScale_; + + this.chart_.style.width = this.chartWidth_ + 'px'; + this.chart_.style.height = this.chartHeight_ + 'px'; + + this.chartCtx_.scale(this.chartScale_, this.chartScale_); + }, + + processPictureData_() { + this.resetOpsTimingData_(); + this.pictureDataProcessed_ = true; + + if (!this.picture_) return; + + let ops = this.picture_.getOps(); + if (!ops) return; + + ops = this.picture_.tagOpsWithTimings(ops); + + // Check that there are valid times. + if (ops[0].cmd_time === undefined) return; + + this.collapseOpsToTimingBuckets_(ops); + }, + + drawChartContents_() { + this.clearChartContents_(); + + if (this.opsTimingData_.length === 0) { + this.showNoTimingDataMessage_(); + return; + } + + this.drawChartAxes_(); + this.drawBars_(); + this.drawLineAtBottomOfChart_(); + + if (this.currentBarMouseOverTarget_ === null) return; + + this.drawTooltip_(); + }, + + drawLineAtBottomOfChart_() { + this.chartCtx_.strokeStyle = '#AAA'; + this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5); + this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5); + this.chartCtx_.stroke(); + }, + + drawTooltip_() { + const tooltipData = this.opsTimingData_[this.currentBarMouseOverTarget_]; + const tooltipTitle = tooltipData.cmd_string; + const tooltipTime = tooltipData.cmd_time.toFixed(4); + + const tooltipWidth = 110; + const tooltipHeight = 40; + const chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT - + CHART_PADDING_LEFT; + const barWidth = chartInnerWidth / this.opsTimingData_.length; + const tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5); + + const left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ * + barWidth - tooltipOffset; + const top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5); + + this.chartCtx_.save(); + + this.chartCtx_.shadowOffsetX = 0; + this.chartCtx_.shadowOffsetY = 5; + this.chartCtx_.shadowBlur = 4; + this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)'; + + this.chartCtx_.strokeStyle = '#888'; + this.chartCtx_.fillStyle = '#EEE'; + this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight); + + this.chartCtx_.shadowColor = 'transparent'; + this.chartCtx_.translate(0.5, 0.5); + this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight); + + this.chartCtx_.restore(); + + this.chartCtx_.fillStyle = '#222'; + this.chartCtx_.textBaseline = 'top'; + this.chartCtx_.font = '800 12px Arial'; + this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8); + + this.chartCtx_.fillStyle = '#555'; + this.chartCtx_.textBaseline = 'top'; + this.chartCtx_.font = '400 italic 10px Arial'; + this.chartCtx_.fillText('Total: ' + tooltipTime + 'ms', + left + 8, top + 22); + }, + + drawBars_() { + const len = this.opsTimingData_.length; + const max = this.opsTimingData_[0].cmd_time; + const min = this.opsTimingData_[len - 1].cmd_time; + + const width = this.chartWidth_ - CHART_PADDING_LEFT - CHART_PADDING_RIGHT; + const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; + const barWidth = Math.floor(width / len); + + let opData; + let opTiming; + let opHeight; + let opLabel; + let barLeft; + + for (let b = 0; b < len; b++) { + opData = this.opsTimingData_[b]; + opTiming = opData.cmd_time / max; + + opHeight = Math.round(Math.max(1, opTiming * height)); + opLabel = opData.cmd_string; + barLeft = CHART_PADDING_LEFT + b * barWidth; + + this.chartCtx_.fillStyle = this.getOpColor_(opLabel); + + this.chartCtx_.fillRect(barLeft + BAR_PADDING, AXIS_PADDING_TOP + + height - opHeight, barWidth - 2 * BAR_PADDING, opHeight); + } + }, + + getOpColor_(opName) { + const characters = opName.split(''); + const hue = characters.reduce(this.reduceNameToHue, 0) % 360; + + return 'hsl(' + hue + ', 30%, 50%)'; + }, + + reduceNameToHue(previousValue, currentValue, index, array) { + // Get the char code and apply a magic adjustment value so we get + // pretty colors from around the rainbow. + return Math.round(previousValue + currentValue.charCodeAt(0) * + HUE_CHAR_CODE_ADJUSTMENT); + }, + + drawChartAxes_() { + const len = this.opsTimingData_.length; + const max = this.opsTimingData_[0].cmd_time; + const min = this.opsTimingData_[len - 1].cmd_time; + + const width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT; + const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; + + const totalBarWidth = this.chartWidth_ - CHART_PADDING_LEFT - + CHART_PADDING_RIGHT; + const barWidth = Math.floor(totalBarWidth / len); + const tickYInterval = height / (VERTICAL_TICKS - 1); + let tickYPosition = 0; + const tickValInterval = (max - min) / (VERTICAL_TICKS - 1); + let tickVal = 0; + + this.chartCtx_.fillStyle = '#333'; + this.chartCtx_.strokeStyle = '#777'; + this.chartCtx_.save(); + + // Translate half a pixel to avoid blurry lines. + this.chartCtx_.translate(0.5, 0.5); + + // Sides. + + this.chartCtx_.save(); + + this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP); + this.chartCtx_.moveTo(0, 0); + this.chartCtx_.lineTo(0, height); + this.chartCtx_.lineTo(width, height); + + // Y-axis ticks. + this.chartCtx_.font = '10px Arial'; + this.chartCtx_.textAlign = 'right'; + this.chartCtx_.textBaseline = 'middle'; + + for (let t = 0; t < VERTICAL_TICKS; t++) { + tickYPosition = Math.round(t * tickYInterval); + tickVal = (max - t * tickValInterval).toFixed(4); + + this.chartCtx_.moveTo(0, tickYPosition); + this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition); + this.chartCtx_.fillText(tickVal, + -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition); + } + + this.chartCtx_.stroke(); + + this.chartCtx_.restore(); + + + // Labels. + + this.chartCtx_.save(); + + this.chartCtx_.translate(CHART_PADDING_LEFT + Math.round(barWidth * 0.5), + AXIS_PADDING_TOP + height + LABEL_PADDING); + + this.chartCtx_.font = '10px Arial'; + this.chartCtx_.textAlign = 'center'; + this.chartCtx_.textBaseline = 'top'; + + let labelTickLeft; + let labelTickBottom; + for (let l = 0; l < len; l++) { + labelTickLeft = Math.round(l * barWidth); + labelTickBottom = l % 2 * LABEL_INTERLEAVE_OFFSET; + + this.chartCtx_.save(); + this.chartCtx_.moveTo(labelTickLeft, -LABEL_PADDING); + this.chartCtx_.lineTo(labelTickLeft, labelTickBottom); + this.chartCtx_.stroke(); + this.chartCtx_.restore(); + + this.chartCtx_.fillText(this.opsTimingData_[l].cmd_string, + labelTickLeft, labelTickBottom); + } + + this.chartCtx_.restore(); + + this.chartCtx_.restore(); + }, + + clearChartContents_() { + this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_); + }, + + showNoTimingDataMessage_() { + this.chartCtx_.font = '800 italic 14px Arial'; + this.chartCtx_.fillStyle = '#333'; + this.chartCtx_.textAlign = 'center'; + this.chartCtx_.textBaseline = 'middle'; + this.chartCtx_.fillText('No timing data available.', + this.chartWidth_ * 0.5, this.chartHeight_ * 0.5); + }, + + collapseOpsToTimingBuckets_(ops) { + const opsTimingDataIndexHash_ = {}; + const timingData = this.opsTimingData_; + let op; + let opIndex; + + for (let i = 0; i < ops.length; i++) { + op = ops[i]; + + if (op.cmd_time === undefined) continue; + + // Try to locate the entry for the current operation + // based on its name. If that fails, then create one for it. + opIndex = opsTimingDataIndexHash_[op.cmd_string] || null; + + if (opIndex === null) { + timingData.push({ + cmd_time: 0, + cmd_string: op.cmd_string + }); + + opIndex = timingData.length - 1; + opsTimingDataIndexHash_[op.cmd_string] = opIndex; + } + + timingData[opIndex].cmd_time += op.cmd_time; + } + + timingData.sort(this.sortTimingBucketsByOpTimeDescending_); + + this.collapseTimingBucketsToOther_(4); + }, + + collapseTimingBucketsToOther_(count) { + const timingData = this.opsTimingData_; + const otherSource = timingData.splice(count, timingData.length - count); + let otherDestination = null; + + if (!otherSource.length) return; + + timingData.push({ + cmd_time: 0, + cmd_string: 'Other' + }); + + otherDestination = timingData[timingData.length - 1]; + for (let i = 0; i < otherSource.length; i++) { + otherDestination.cmd_time += otherSource[i].cmd_time; + } + }, + + sortTimingBucketsByOpTimeDescending_(a, b) { + return b.cmd_time - a.cmd_time; + }, + + resetOpsTimingData_() { + this.opsTimingData_.length = 0; + } + }; + + return { + PictureOpsChartSummaryView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html new file mode 100644 index 00000000000..413998847aa --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html @@ -0,0 +1,505 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const BAR_PADDING = 1; + const BAR_WIDTH = 5; + const CHART_PADDING_LEFT = 65; + const CHART_PADDING_RIGHT = 30; + const CHART_PADDING_BOTTOM = 35; + const CHART_PADDING_TOP = 20; + const AXIS_PADDING_LEFT = 55; + const AXIS_PADDING_RIGHT = 30; + const AXIS_PADDING_BOTTOM = 35; + const AXIS_PADDING_TOP = 20; + const AXIS_TICK_SIZE = 5; + const AXIS_LABEL_PADDING = 5; + const VERTICAL_TICKS = 5; + const HUE_CHAR_CODE_ADJUSTMENT = 5.7; + + /** + * Provides a chart showing the cumulative time spent in Skia operations + * during picture rasterization. + * + * @constructor + */ + const PictureOpsChartView = + tr.ui.b.define('tr-ui-e-chrome-cc-picture-ops-chart-view'); + + PictureOpsChartView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.style.display = 'block'; + this.style.height = '180px'; + this.style.margin = 0; + this.style.padding = 0; + this.style.position = 'relative'; + + this.picture_ = undefined; + this.pictureOps_ = undefined; + this.opCosts_ = undefined; + + this.chartScale_ = window.devicePixelRatio; + + this.chart_ = document.createElement('canvas'); + this.chartCtx_ = this.chart_.getContext('2d'); + Polymer.dom(this).appendChild(this.chart_); + + this.selectedOpIndex_ = undefined; + this.chartWidth_ = 0; + this.chartHeight_ = 0; + this.dimensionsHaveChanged_ = true; + + this.currentBarMouseOverTarget_ = undefined; + + this.ninetyFifthPercentileCost_ = 0; + this.totalOpCost_ = 0; + + this.chart_.addEventListener('click', this.onClick_.bind(this)); + this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this)); + new ResizeObserver(this.onResize_.bind(this)).observe(this); + + this.usePercentileScale_ = false; + this.usePercentileScaleCheckbox_ = tr.ui.b.createCheckBox( + this, 'usePercentileScale', + 'PictureOpsChartView.usePercentileScale', false, + 'Limit to 95%-ile'); + Polymer.dom(this.usePercentileScaleCheckbox_).classList.add( + 'use-percentile-scale'); + this.usePercentileScaleCheckbox_.style.position = 'absolute'; + this.usePercentileScaleCheckbox_.style.left = 0; + this.usePercentileScaleCheckbox_.style.top = 0; + Polymer.dom(this).appendChild(this.usePercentileScaleCheckbox_); + }, + + get dimensionsHaveChanged() { + return this.dimensionsHaveChanged_; + }, + + set dimensionsHaveChanged(dimensionsHaveChanged) { + this.dimensionsHaveChanged_ = dimensionsHaveChanged; + }, + + get usePercentileScale() { + return this.usePercentileScale_; + }, + + set usePercentileScale(usePercentileScale) { + this.usePercentileScale_ = usePercentileScale; + this.drawChartContents_(); + }, + + get numOps() { + return this.opCosts_.length; + }, + + get selectedOpIndex() { + return this.selectedOpIndex_; + }, + + set selectedOpIndex(selectedOpIndex) { + if (selectedOpIndex < 0) throw new Error('Invalid index'); + if (selectedOpIndex >= this.numOps) throw new Error('Invalid index'); + + this.selectedOpIndex_ = selectedOpIndex; + }, + + get picture() { + return this.picture_; + }, + + set picture(picture) { + this.picture_ = picture; + this.pictureOps_ = picture.tagOpsWithTimings(picture.getOps()); + this.currentBarMouseOverTarget_ = undefined; + this.processPictureData_(); + this.dimensionsHaveChanged = true; + }, + + processPictureData_() { + if (this.pictureOps_ === undefined) return; + + let totalOpCost = 0; + + // Take a copy of the picture ops data for sorting. + this.opCosts_ = this.pictureOps_.map(function(op) { + totalOpCost += op.cmd_time; + return op.cmd_time; + }); + this.opCosts_.sort(); + + const ninetyFifthPercentileCostIndex = Math.floor( + this.opCosts_.length * 0.95); + this.ninetyFifthPercentileCost_ = + this.opCosts_[ninetyFifthPercentileCostIndex]; + this.maxCost_ = this.opCosts_[this.opCosts_.length - 1]; + + this.totalOpCost_ = totalOpCost; + }, + + extractBarIndex_(e) { + let index = undefined; + + if (this.pictureOps_ === undefined || + this.pictureOps_.length === 0) { + return index; + } + + const x = e.offsetX; + const y = e.offsetY; + + const totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length; + + const chartLeft = CHART_PADDING_LEFT; + const chartTop = 0; + const chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM; + const chartRight = chartLeft + totalBarWidth; + + if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom) { + return index; + } + + index = Math.floor((x - chartLeft) / totalBarWidth * + this.pictureOps_.length); + + index = tr.b.math.clamp(index, 0, this.pictureOps_.length - 1); + + return index; + }, + + onClick_(e) { + const barClicked = this.extractBarIndex_(e); + + if (barClicked === undefined) return; + + // If we click on the already selected item we should deselect. + if (barClicked === this.selectedOpIndex) { + this.selectedOpIndex = undefined; + } else { + this.selectedOpIndex = barClicked; + } + + e.preventDefault(); + + tr.b.dispatchSimpleEvent(this, 'selection-changed', false); + }, + + onMouseMove_(e) { + const lastBarMouseOverTarget = this.currentBarMouseOverTarget_; + this.currentBarMouseOverTarget_ = this.extractBarIndex_(e); + + if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget) { + return; + } + + this.drawChartContents_(); + }, + + onResize_() { + this.dimensionsHaveChanged = true; + this.updateChartContents(); + }, + + scrollSelectedItemIntoViewIfNecessary() { + if (this.selectedOpIndex === undefined) { + return; + } + + const width = this.offsetWidth; + const left = this.scrollLeft; + const right = left + width; + const targetLeft = CHART_PADDING_LEFT + + (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex; + + if (targetLeft > left && targetLeft < right) { + return; + } + + this.scrollLeft = (targetLeft - width * 0.5); + }, + + updateChartContents() { + if (this.dimensionsHaveChanged) { + this.updateChartDimensions_(); + } + + this.drawChartContents_(); + }, + + updateChartDimensions_() { + if (!this.pictureOps_) return; + + let width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT + + ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length); + + if (width < this.offsetWidth) { + width = this.offsetWidth; + } + + // Allow the element to be its natural size as set by flexbox, then lock + // the width in before we set the width of the canvas. + this.chartWidth_ = width; + this.chartHeight_ = this.getBoundingClientRect().height; + + // Scale up the canvas according to the devicePixelRatio, then reduce it + // down again via CSS. Finally we apply a scale to the canvas so that + // things are drawn at the correct size. + this.chart_.width = this.chartWidth_ * this.chartScale_; + this.chart_.height = this.chartHeight_ * this.chartScale_; + + this.chart_.style.width = this.chartWidth_ + 'px'; + this.chart_.style.height = this.chartHeight_ + 'px'; + + this.chartCtx_.scale(this.chartScale_, this.chartScale_); + + this.dimensionsHaveChanged = false; + }, + + drawChartContents_() { + this.clearChartContents_(); + + if (this.pictureOps_ === undefined || + this.pictureOps_.length === 0 || + this.pictureOps_[0].cmd_time === undefined) { + this.showNoTimingDataMessage_(); + return; + } + + this.drawSelection_(); + this.drawBars_(); + this.drawChartAxes_(); + this.drawLinesAtTickMarks_(); + this.drawLineAtBottomOfChart_(); + + if (this.currentBarMouseOverTarget_ === undefined) { + return; + } + + this.drawTooltip_(); + }, + + drawSelection_() { + if (this.selectedOpIndex === undefined) { + return; + } + + const width = (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex; + this.chartCtx_.fillStyle = 'rgb(223, 235, 230)'; + this.chartCtx_.fillRect(CHART_PADDING_LEFT, CHART_PADDING_TOP, + width, this.chartHeight_ - CHART_PADDING_TOP - CHART_PADDING_BOTTOM); + }, + + drawChartAxes_() { + const min = this.opCosts_[0]; + const max = this.opCosts_[this.opCosts_.length - 1]; + const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; + + const tickYInterval = height / (VERTICAL_TICKS - 1); + let tickYPosition = 0; + const tickValInterval = (max - min) / (VERTICAL_TICKS - 1); + let tickVal = 0; + + this.chartCtx_.fillStyle = '#333'; + this.chartCtx_.strokeStyle = '#777'; + this.chartCtx_.save(); + + // Translate half a pixel to avoid blurry lines. + this.chartCtx_.translate(0.5, 0.5); + + // Sides. + this.chartCtx_.beginPath(); + this.chartCtx_.moveTo(AXIS_PADDING_LEFT, AXIS_PADDING_TOP); + this.chartCtx_.lineTo(AXIS_PADDING_LEFT, this.chartHeight_ - + AXIS_PADDING_BOTTOM); + this.chartCtx_.lineTo(this.chartWidth_ - AXIS_PADDING_RIGHT, + this.chartHeight_ - AXIS_PADDING_BOTTOM); + this.chartCtx_.stroke(); + this.chartCtx_.closePath(); + + // Y-axis ticks. + this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP); + + this.chartCtx_.font = '10px Arial'; + this.chartCtx_.textAlign = 'right'; + this.chartCtx_.textBaseline = 'middle'; + + this.chartCtx_.beginPath(); + for (let t = 0; t < VERTICAL_TICKS; t++) { + tickYPosition = Math.round(t * tickYInterval); + tickVal = (max - t * tickValInterval).toFixed(4); + + this.chartCtx_.moveTo(0, tickYPosition); + this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition); + this.chartCtx_.fillText(tickVal, + -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition); + } + + this.chartCtx_.stroke(); + this.chartCtx_.closePath(); + + this.chartCtx_.restore(); + }, + + drawLinesAtTickMarks_() { + const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; + const width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT; + const tickYInterval = height / (VERTICAL_TICKS - 1); + let tickYPosition = 0; + + this.chartCtx_.save(); + + this.chartCtx_.translate(AXIS_PADDING_LEFT + 0.5, AXIS_PADDING_TOP + 0.5); + this.chartCtx_.beginPath(); + this.chartCtx_.strokeStyle = 'rgba(0,0,0,0.05)'; + + for (let t = 0; t < VERTICAL_TICKS; t++) { + tickYPosition = Math.round(t * tickYInterval); + + this.chartCtx_.moveTo(0, tickYPosition); + this.chartCtx_.lineTo(width, tickYPosition); + this.chartCtx_.stroke(); + } + + this.chartCtx_.restore(); + this.chartCtx_.closePath(); + }, + + drawLineAtBottomOfChart_() { + this.chartCtx_.strokeStyle = '#AAA'; + this.chartCtx_.beginPath(); + this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5); + this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5); + this.chartCtx_.stroke(); + this.chartCtx_.closePath(); + }, + + drawTooltip_() { + const tooltipData = this.pictureOps_[this.currentBarMouseOverTarget_]; + const tooltipTitle = tooltipData.cmd_string; + const tooltipTime = tooltipData.cmd_time.toFixed(4); + const toolTipTimePercentage = + ((tooltipData.cmd_time / this.totalOpCost_) * 100).toFixed(2); + + const tooltipWidth = 120; + const tooltipHeight = 40; + const chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT - + CHART_PADDING_LEFT; + const barWidth = BAR_WIDTH + BAR_PADDING; + const tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5); + + const left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ * + barWidth - tooltipOffset; + const top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5); + + this.chartCtx_.save(); + + this.chartCtx_.shadowOffsetX = 0; + this.chartCtx_.shadowOffsetY = 5; + this.chartCtx_.shadowBlur = 4; + this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)'; + + this.chartCtx_.strokeStyle = '#888'; + this.chartCtx_.fillStyle = '#EEE'; + this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight); + + this.chartCtx_.shadowColor = 'transparent'; + this.chartCtx_.translate(0.5, 0.5); + this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight); + + this.chartCtx_.restore(); + + this.chartCtx_.fillStyle = '#222'; + this.chartCtx_.textAlign = 'left'; + this.chartCtx_.textBaseline = 'top'; + this.chartCtx_.font = '800 12px Arial'; + this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8); + + this.chartCtx_.fillStyle = '#555'; + this.chartCtx_.font = '400 italic 10px Arial'; + this.chartCtx_.fillText(tooltipTime + 'ms (' + + toolTipTimePercentage + '%)', left + 8, top + 22); + }, + + drawBars_() { + let op; + let opColor = 0; + let opHeight = 0; + const opWidth = BAR_WIDTH + BAR_PADDING; + let opHover = false; + + const bottom = this.chartHeight_ - CHART_PADDING_BOTTOM; + const maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM - + CHART_PADDING_TOP; + + let maxValue; + if (this.usePercentileScale) { + maxValue = this.ninetyFifthPercentileCost_; + } else { + maxValue = this.maxCost_; + } + + for (let b = 0; b < this.pictureOps_.length; b++) { + op = this.pictureOps_[b]; + opHeight = Math.round( + (op.cmd_time / maxValue) * maxHeight); + opHeight = Math.max(opHeight, 1); + opHover = (b === this.currentBarMouseOverTarget_); + opColor = this.getOpColor_(op.cmd_string, opHover); + + if (b === this.selectedOpIndex) { + this.chartCtx_.fillStyle = '#FFFF00'; + } else { + this.chartCtx_.fillStyle = opColor; + } + + this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth, + bottom - opHeight, BAR_WIDTH, opHeight); + } + }, + + getOpColor_(opName, hover) { + const characters = opName.split(''); + + const hue = characters.reduce(this.reduceNameToHue, 0) % 360; + const saturation = 30; + const lightness = hover ? '75%' : '50%'; + + return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)'; + }, + + reduceNameToHue(previousValue, currentValue, index, array) { + // Get the char code and apply a magic adjustment value so we get + // pretty colors from around the rainbow. + return Math.round(previousValue + currentValue.charCodeAt(0) * + HUE_CHAR_CODE_ADJUSTMENT); + }, + + clearChartContents_() { + this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_); + }, + + showNoTimingDataMessage_() { + this.chartCtx_.font = '800 italic 14px Arial'; + this.chartCtx_.fillStyle = '#333'; + this.chartCtx_.textAlign = 'center'; + this.chartCtx_.textBaseline = 'middle'; + this.chartCtx_.fillText('No timing data available.', + this.chartWidth_ * 0.5, this.chartHeight_ * 0.5); + } + }; + + return { + PictureOpsChartView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html new file mode 100644 index 00000000000..2e45be58c33 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html @@ -0,0 +1,261 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/chrome/cc/constants.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/list_view.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const OPS_TIMING_ITERATIONS = 3; // Iterations to average op timing info over. + const ANNOTATION = 'Comment'; + const BEGIN_ANNOTATION = 'BeginCommentGroup'; + const END_ANNOTATION = 'EndCommentGroup'; + const ANNOTATION_ID = 'ID: '; + const ANNOTATION_CLASS = 'CLASS: '; + const ANNOTATION_TAG = 'TAG: '; + + const constants = tr.e.cc.constants; + + /** + * @constructor + */ + const PictureOpsListView = + tr.ui.b.define('tr-ui-e-chrome-cc-picture-ops-list-view'); + + PictureOpsListView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.style.borderTop = '1px solid grey'; + this.style.overflow = 'auto'; + this.opsList_ = new tr.ui.b.ListView(); + Polymer.dom(this).appendChild(this.opsList_); + + this.selectedOp_ = undefined; + this.selectedOpIndex_ = undefined; + this.opsList_.addEventListener( + 'selection-changed', this.onSelectionChanged_.bind(this)); + + this.picture_ = undefined; + }, + + get picture() { + return this.picture_; + }, + + set picture(picture) { + this.picture_ = picture; + this.updateContents_(); + }, + + updateContents_() { + this.opsList_.clear(); + + if (!this.picture_) return; + + let ops = this.picture_.getOps(); + if (!ops) return; + + ops = this.picture_.tagOpsWithTimings(ops); + + ops = this.opsTaggedWithAnnotations_(ops); + + for (let i = 0; i < ops.length; i++) { + const op = ops[i]; + const item = document.createElement('div'); + item.opIndex = op.opIndex; + Polymer.dom(item).textContent = i + ') ' + op.cmd_string; + + // Display the element info associated with the op, if available. + if (op.elementInfo.tag || op.elementInfo.id || op.elementInfo.class) { + const elementInfo = document.createElement('span'); + Polymer.dom(elementInfo).classList.add('elementInfo'); + elementInfo.style.color = 'purple'; + elementInfo.style.fontSize = 'small'; + elementInfo.style.fontWeight = 'bold'; + elementInfo.style.color = '#777'; + const tag = op.elementInfo.tag ? op.elementInfo.tag : 'unknown'; + const id = op.elementInfo.id ? 'id=' + op.elementInfo.id : undefined; + const className = op.elementInfo.class ? 'class=' + + op.elementInfo.class : undefined; + Polymer.dom(elementInfo).textContent = + '<' + tag + (id ? ' ' : '') + + (id ? id : '') + (className ? ' ' : '') + + (className ? className : '') + '>'; + Polymer.dom(item).appendChild(elementInfo); + } + + // Display the Skia params. + // FIXME: now that we have structured data, we should format it. + // (https://github.com/google/trace-viewer/issues/782) + if (op.info.length > 0) { + const infoItem = document.createElement('div'); + Polymer.dom(infoItem).textContent = JSON.stringify(op.info); + infoItem.style.fontSize = 'x-small'; + infoItem.style.color = '#777'; + Polymer.dom(item).appendChild(infoItem); + } + + // Display the op timing, if available. + if (op.cmd_time && op.cmd_time >= 0.0001) { + const time = document.createElement('span'); + Polymer.dom(time).classList.add('time'); + const rounded = op.cmd_time.toFixed(4); + Polymer.dom(time).textContent = '(' + rounded + 'ms)'; + time.style.fontSize = 'x-small'; + time.style.color = 'rgb(136, 0, 0)'; + Polymer.dom(item).appendChild(time); + } + + item.style.borderBottom = '1px solid #555'; + item.style.fontSize = 'small'; + item.style.fontWeight = 'bold'; + item.style.paddingBottom = '5px'; + item.style.paddingLeft = '5px'; + item.style.cursor = 'pointer'; + + for (const child of item.children) { + child.style.fontWeight = 'normal'; + child.style.marginLeft = '1em'; + child.style.maxWidth = '300px'; + } + + Polymer.dom(this.opsList_).appendChild(item); + } + }, + + onSelectionChanged_(e) { + let beforeSelectedOp = true; + + // Deselect on re-selection. + if (this.opsList_.selectedElement === this.selectedOp_) { + this.opsList_.selectedElement = undefined; + beforeSelectedOp = false; + this.selectedOpIndex_ = undefined; + } + + this.selectedOp_ = this.opsList_.selectedElement; + + // Set selection on all previous ops. + const ops = this.opsList_.children; + for (let i = 0; i < ops.length; i++) { + const op = ops[i]; + if (op === this.selectedOp_) { + beforeSelectedOp = false; + this.selectedOpIndex_ = op.opIndex; + } else if (beforeSelectedOp) { + Polymer.dom(op).setAttribute('beforeSelection', 'beforeSelection'); + op.style.backgroundColor = 'rgb(103, 199, 165)'; + } else { + Polymer.dom(op).removeAttribute('beforeSelection'); + op.style.backgroundColor = ''; + } + } + + tr.b.dispatchSimpleEvent(this, 'selection-changed', false); + }, + + get numOps() { + return this.opsList_.children.length; + }, + + get selectedOpIndex() { + return this.selectedOpIndex_; + }, + + set selectedOpIndex(s) { + this.selectedOpIndex_ = s; + + if (s === undefined) { + this.opsList_.selectedElement = this.selectedOp_; + this.onSelectionChanged_(); + } else { + if (s < 0) throw new Error('Invalid index'); + if (s >= this.numOps) throw new Error('Invalid index'); + this.opsList_.selectedElement = this.opsList_.getElementByIndex(s + 1); + tr.ui.b.scrollIntoViewIfNeeded(this.opsList_.selectedElement); + } + }, + + /** + * Return Skia operations tagged by annotation. + * + * The ops returned from Picture.getOps() contain both Skia ops and + * annotations threaded together. This function removes all annotations + * from the list and tags each op with the associated annotations. + * Additionally, the last {tag, id, class} is stored as elementInfo on + * each op. + * + * @param {Array} ops Array of Skia operations and annotations. + * @return {Array} Skia ops where op.annotations contains the associated + * annotations for a given op. + */ + opsTaggedWithAnnotations_(ops) { + // This algorithm works by walking all the ops and pushing any + // annotations onto a stack. When a non-annotation op is found, the + // annotations stack is traversed and stored with the op. + const annotationGroups = []; + const opsWithoutAnnotations = []; + for (let opIndex = 0; opIndex < ops.length; opIndex++) { + const op = ops[opIndex]; + op.opIndex = opIndex; + switch (op.cmd_string) { + case BEGIN_ANNOTATION: + annotationGroups.push([]); + break; + case END_ANNOTATION: + annotationGroups.pop(); + break; + case ANNOTATION: + annotationGroups[annotationGroups.length - 1].push(op); + break; + default: { + const annotations = []; + let elementInfo = {}; + annotationGroups.forEach(function(annotationGroup) { + elementInfo = {}; + annotationGroup.forEach(function(annotation) { + annotation.info.forEach(function(info) { + if (info.includes(ANNOTATION_TAG)) { + elementInfo.tag = info.substring( + info.indexOf(ANNOTATION_TAG) + + ANNOTATION_TAG.length).toLowerCase(); + } else if (info.includes(ANNOTATION_ID)) { + elementInfo.id = info.substring( + info.indexOf(ANNOTATION_ID) + + ANNOTATION_ID.length); + } else if (info.includes(ANNOTATION_CLASS)) { + elementInfo.class = info.substring( + info.indexOf(ANNOTATION_CLASS) + + ANNOTATION_CLASS.length); + } + + annotations.push(info); + }); + }); + }); + op.annotations = annotations; + op.elementInfo = elementInfo; + opsWithoutAnnotations.push(op); + } + } + } + + return opsWithoutAnnotations; + } + }; + + return { + PictureOpsListView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html new file mode 100644 index 00000000000..b58c1568f4f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_list_view.html"> + +<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js"> +</script> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const PictureOpsListView = tr.ui.e.chrome.cc.PictureOpsListView; + + test('instantiate', function() { + if (!tr.e.cc.PictureSnapshot.CanRasterize()) return; + + const m = new tr.Model(g_catLTHIEvents); + const p = Object.values(m.processes)[0]; + + const instance = p.objects.getAllInstancesNamed('cc::Picture')[0]; + const snapshot = instance.snapshots[0]; + + const view = new PictureOpsListView(); + view.picture = snapshot; + assert.strictEqual(view.opsList_.children.length, 142); + }); + + test('selection', function() { + if (!tr.e.cc.PictureSnapshot.CanRasterize()) return; + + const m = new tr.Model(g_catLTHIEvents); + const p = Object.values(m.processes)[0]; + + const instance = p.objects.getAllInstancesNamed('cc::Picture')[0]; + const snapshot = instance.snapshots[0]; + + const view = new PictureOpsListView(); + view.picture = snapshot; + let didSelectionChange = 0; + view.addEventListener('selection-changed', function() { + didSelectionChange = true; + }); + assert.isFalse(didSelectionChange); + view.opsList_.selectedElement = view.opsList_.children[3]; + assert.isTrue(didSelectionChange); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html new file mode 100644 index 00000000000..a9db575773f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_debugger.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + /* + * Displays a picture snapshot in a human readable form. + * @constructor + */ + const PictureSnapshotView = tr.ui.b.define( + 'tr-ui-e-chrome-cc-picture-snapshot-view', + tr.ui.analysis.ObjectSnapshotView); + + PictureSnapshotView.prototype = { + __proto__: tr.ui.analysis.ObjectSnapshotView.prototype, + + decorate() { + Polymer.dom(this).classList.add( + 'tr-ui-e-chrome-cc-picture-snapshot-view'); + this.style.display = 'flex'; + this.style.flexGrow = 1; + this.style.flexShrink = 1; + this.style.flexBasis = 'auto'; + this.style.minWidth = 0; + this.pictureDebugger_ = new tr.ui.e.chrome.cc.PictureDebugger(); + this.pictureDebugger_.style.flexGrow = 1; + this.pictureDebugger_.style.flexShrink = 1; + this.pictureDebugger_.style.flexBasis = 'auto'; + this.pictureDebugger_.style.minWidth = 0; + Polymer.dom(this).appendChild(this.pictureDebugger_); + }, + + updateContents() { + if (this.objectSnapshot_ && this.pictureDebugger_) { + this.pictureDebugger_.picture = this.objectSnapshot_; + } + } + }; + + tr.ui.analysis.ObjectSnapshotView.register( + PictureSnapshotView, + { + typeNames: ['cc::Picture', 'cc::LayeredPicture'], + showInstances: false + }); + + return { + PictureSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html new file mode 100644 index 00000000000..6b1a7cb7df0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html @@ -0,0 +1,140 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/extras/chrome/cc/raster_task.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + /** + * @constructor + */ + function RasterTaskSelection(selection) { + tr.ui.e.chrome.cc.Selection.call(this); + const whySupported = RasterTaskSelection.whySuported(selection); + if (!whySupported.ok) { + throw new Error('Fail: ' + whySupported.why); + } + this.slices_ = Array.from(selection); + this.tiles_ = this.slices_.map(function(slice) { + const tile = tr.e.cc.getTileFromRasterTaskSlice(slice); + if (tile === undefined) { + throw new Error('This should never happen due to .supports check.'); + } + return tile; + }); + } + + RasterTaskSelection.whySuported = function(selection) { + if (!(selection instanceof tr.model.EventSet)) { + return {ok: false, why: 'Must be selection'}; + } + + if (selection.length === 0) { + return {ok: false, why: 'Selection must be non empty'}; + } + + let referenceSnapshot = undefined; + for (const event of selection) { + if (!(event instanceof tr.model.Slice)) { + return {ok: false, why: 'Not a slice'}; + } + + const tile = tr.e.cc.getTileFromRasterTaskSlice(event); + if (tile === undefined) { + return {ok: false, why: 'No tile found'}; + } + + if (!referenceSnapshot) { + referenceSnapshot = tile.containingSnapshot; + } else { + if (tile.containingSnapshot !== referenceSnapshot) { + return { + ok: false, + why: 'Raster tasks are from different compositor instances' + }; + } + } + } + return {ok: true}; + }; + + RasterTaskSelection.supports = function(selection) { + return RasterTaskSelection.whySuported(selection).ok; + }; + + RasterTaskSelection.prototype = { + __proto__: tr.ui.e.chrome.cc.Selection.prototype, + + get specicifity() { + return 3; + }, + + get associatedLayerId() { + const tile0 = this.tiles_[0]; + const allSameLayer = this.tiles_.every(function(tile) { + tile.layerId === tile0.layerId; + }); + if (allSameLayer) { + return tile0.layerId; + } + return undefined; + }, + + get extraHighlightsByLayerId() { + const highlights = {}; + this.tiles_.forEach(function(tile, i) { + if (highlights[tile.layerId] === undefined) { + highlights[tile.layerId] = []; + } + const slice = this.slices_[i]; + highlights[tile.layerId].push({ + colorKey: slice.title, + rect: tile.layerRect + }); + }, this); + return highlights; + }, + + createAnalysis() { + const sel = new tr.model.EventSet(); + this.slices_.forEach(function(slice) { + sel.push(slice); + }); + + let analysis; + if (sel.length === 1) { + analysis = document.createElement('tr-ui-a-single-event-sub-view'); + } else { + analysis = document.createElement('tr-ui-e-chrome-cc-raster-task-view'); + } + analysis.selection = sel; + return analysis; + }, + + findEquivalent(lthi) { + // Raster tasks are only valid in one LTHI. + return undefined; + }, + + // RasterTaskSelection specific stuff follows. + get containingSnapshot() { + return this.tiles_[0].containingSnapshot; + } + }; + + return { + RasterTaskSelection, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html new file mode 100644 index 00000000000..d95a7135d37 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_selection.html"> + +<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js"> +</script> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('basic', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + const rasterTasks = p.threads[1].sliceGroup.slices.filter(function(slice) { + return slice.title === 'RasterTask'; + }); + + let selection = new tr.model.EventSet(); + selection.push(rasterTasks[0]); + selection.push(rasterTasks[1]); + + assert.isTrue(tr.ui.e.chrome.cc.RasterTaskSelection.supports(selection)); + selection = new tr.ui.e.chrome.cc.RasterTaskSelection(selection); + const highlights = selection.extraHighlightsByLayerId; + assert.lengthOf(Object.keys(highlights), 1); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html new file mode 100644 index 00000000000..a5f7f5d806c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html @@ -0,0 +1,222 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/extras/chrome/cc/raster_task.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-e-chrome-cc-raster-task-view'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + #heading { + flex: 0 0 auto; + } + tr-ui-b-table { + font-size: 12px; + } + </style> + + <div id="heading"> + Rasterization costs in + <tr-ui-a-analysis-link id="link"></tr-ui-a-analysis-link> + </div> + <tr-ui-b-table id="content"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; +Polymer({ + is: 'tr-ui-e-chrome-cc-raster-task-view', + + created() { + this.selection_ = undefined; + }, + + set selection(selection) { + this.selection_ = selection; + + this.updateContents_(); + }, + + updateColumns_(hadCpuDurations) { + const timeSpanConfig = { + unit: tr.b.Unit.byName.timeDurationInMs, + ownerDocument: this.ownerDocument + }; + + const columns = [ + { + title: 'Layer', + value(row) { + if (row.isTotals) return 'Totals'; + if (row.layer) { + const linkEl = document.createElement('tr-ui-a-analysis-link'); + linkEl.setSelectionAndContent( + function() { + return new tr.ui.e.chrome.cc.LayerSelection(row.layer); + }, + 'Layer ' + row.layerId); + return linkEl; + } + return 'Layer ' + row.layerId; + }, + width: '250px' + }, + { + title: 'Num Tiles', + value(row) { return row.numTiles; }, + cmp(a, b) { return a.numTiles - b.numTiles; } + }, + { + title: 'Num Analysis Tasks', + value(row) { return row.numAnalysisTasks; }, + cmp(a, b) { + return a.numAnalysisTasks - b.numAnalysisTasks; + } + }, + { + title: 'Num Raster Tasks', + value(row) { return row.numRasterTasks; }, + cmp(a, b) { return a.numRasterTasks - b.numRasterTasks; } + }, + { + title: 'Wall Duration (ms)', + value(row) { + return tr.v.ui.createScalarSpan(row.duration, timeSpanConfig); + }, + cmp(a, b) { return a.duration - b.duration; } + } + ]; + + if (hadCpuDurations) { + columns.push({ + title: 'CPU Duration (ms)', + value(row) { + return tr.v.ui.createScalarSpan(row.cpuDuration, timeSpanConfig); + }, + cmp(a, b) { return a.cpuDuration - b.cpuDuration; } + }); + } + + let colWidthPercentage; + if (columns.length === 1) { + colWidthPercentage = '100%'; + } else { + colWidthPercentage = (100 / (columns.length - 1)).toFixed(3) + '%'; + } + + for (let i = 1; i < columns.length; i++) { + columns[i].width = colWidthPercentage; + } + + this.$.content.tableColumns = columns; + this.$.content.sortColumnIndex = columns.length - 1; + }, + + updateContents_() { + const table = this.$.content; + + if (this.selection_.length === 0) { + this.$.link.setSelectionAndContent(undefined, ''); + table.tableRows = []; + table.footerRows = []; + table.rebuild(); + return; + } + // LTHI link. + const lthi = tr.e.cc.getTileFromRasterTaskSlice( + tr.b.getFirstElement(this.selection_)).containingSnapshot; + this.$.link.setSelectionAndContent(function() { + return new tr.model.EventSet(lthi); + }, lthi.userFriendlyName); + + // Get costs by layer. + const costsByLayerId = {}; + function getCurrentCostsForLayerId(tile) { + const layerId = tile.layerId; + const lthi = tile.containingSnapshot; + let layer; + if (lthi.activeTree) { + layer = lthi.activeTree.findLayerWithId(layerId); + } + if (layer === undefined && lthi.pendingTree) { + layer = lthi.pendingTree.findLayerWithId(layerId); + } + if (costsByLayerId[layerId] === undefined) { + costsByLayerId[layerId] = { + layerId, + layer, + numTiles: 0, + numAnalysisTasks: 0, + numRasterTasks: 0, + duration: 0, + cpuDuration: 0 + }; + } + return costsByLayerId[layerId]; + } + + let totalDuration = 0; + let totalCpuDuration = 0; + let totalNumAnalyzeTasks = 0; + let totalNumRasterizeTasks = 0; + let hadCpuDurations = false; + + const tilesThatWeHaveSeen = {}; + + this.selection_.forEach(function(slice) { + const tile = tr.e.cc.getTileFromRasterTaskSlice(slice); + const curCosts = getCurrentCostsForLayerId(tile); + + if (!tilesThatWeHaveSeen[tile.objectInstance.id]) { + tilesThatWeHaveSeen[tile.objectInstance.id] = true; + curCosts.numTiles += 1; + } + + if (tr.e.cc.isSliceDoingAnalysis(slice)) { + curCosts.numAnalysisTasks += 1; + totalNumAnalyzeTasks += 1; + } else { + curCosts.numRasterTasks += 1; + totalNumRasterizeTasks += 1; + } + curCosts.duration += slice.duration; + totalDuration += slice.duration; + if (slice.cpuDuration !== undefined) { + curCosts.cpuDuration += slice.cpuDuration; + totalCpuDuration += slice.cpuDuration; + hadCpuDurations = true; + } + }); + + // Apply to the table. + this.updateColumns_(hadCpuDurations); + table.tableRows = Object.values(costsByLayerId); + table.rebuild(); + + // Footer. + table.footerRows = [ + { + isTotals: true, + numTiles: Object.keys(tilesThatWeHaveSeen).length, + numAnalysisTasks: totalNumAnalyzeTasks, + numRasterTasks: totalNumRasterizeTasks, + duration: totalDuration, + cpuDuration: totalCpuDuration + } + ]; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html new file mode 100644 index 00000000000..56767cfcc89 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/event_target.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_view.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/ui/brushing_state_controller.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_selection.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_view.html"> + +<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js"> +</script> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createSelection() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + const rasterTasks = p.threads[1].sliceGroup.slices.filter(function(slice) { + return slice.title === 'RasterTask' || slice.title === 'AnalyzeTask'; + }); + + const selection = new tr.model.EventSet(); + selection.model = m; + + selection.push(rasterTasks[0]); + selection.push(rasterTasks[1]); + return selection; + } + + test('basic', function() { + const selection = createSelection(); + const view = document.createElement('tr-ui-e-chrome-cc-raster-task-view'); + view.selection = selection; + this.addHTMLOutput(view); + }); + + test('analysisViewIntegration', function() { + const selection = createSelection(); + + const timelineView = {model: selection.model}; + const brushingStateController = + new tr.c.BrushingStateController(timelineView); + + const analysisEl = document.createElement('tr-ui-a-analysis-view'); + analysisEl.brushingStateController = brushingStateController; + brushingStateController.changeSelectionFromTimeline(selection); + + assert.isDefined(Polymer.dom(analysisEl).querySelector( + 'tr-ui-e-chrome-cc-raster-task-view')); + + const sv = tr.ui.b.findDeepElementMatching( + analysisEl, 'tr-ui-a-multi-thread-slice-sub-view'); + assert.isTrue(sv.requiresTallView); + this.addHTMLOutput(analysisEl); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html new file mode 100644 index 00000000000..2794540e115 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html @@ -0,0 +1,304 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + function Selection() { + this.selectionToSetIfClicked = undefined; + } + Selection.prototype = { + /** + * When two things are picked in the UI, one must occasionally tie-break + * between them to decide what was really clicked. Things with higher + * specicifity will win. + */ + get specicifity() { + throw new Error('Not implemented'); + }, + + /** + * If a selection is related to a specific layer, then this returns the + * layerId of that layer. If the selection is not related to a layer, for + * example if the device viewport is selected, then this returns undefined. + */ + get associatedLayerId() { + throw new Error('Not implemented'); + }, + + /** + * If a selection is related to a specific render pass, then this returns + * the layerId of that layer. If the selection is not related to a layer, + * for example if the device viewport is selected, then this returns + * undefined. + */ + get associatedRenderPassId() { + throw new Error('Not implemented'); + }, + + + get highlightsByLayerId() { + return {}; + }, + + /** + * Called when the selection is made active in the layer view. Must return + * an HTMLElement that explains this selection in detail. + */ + createAnalysis() { + throw new Error('Not implemented'); + }, + + /** + * Should try to create the equivalent selection in the provided LTHI, + * or undefined if it can't be done. + */ + findEquivalent(lthi) { + throw new Error('Not implemented'); + } + }; + + /** + * @constructor + */ + function RenderPassSelection(renderPass, renderPassId) { + if (!renderPass || (renderPassId === undefined)) { + throw new Error('Render pass (with id) is required'); + } + this.renderPass_ = renderPass; + this.renderPassId_ = renderPassId; + } + + RenderPassSelection.prototype = { + __proto__: Selection.prototype, + + get specicifity() { + return 1; + }, + + get associatedLayerId() { + return undefined; + }, + + get associatedRenderPassId() { + return this.renderPassId_; + }, + + get renderPass() { + return this.renderPass_; + }, + + createAnalysis() { + const dataView = document.createElement( + 'tr-ui-a-generic-object-view-with-label'); + dataView.label = 'RenderPass ' + this.renderPassId_; + dataView.object = this.renderPass_.args; + return dataView; + }, + + get title() { + return this.renderPass_.objectInstance.typeName; + } + }; + + /** + * @constructor + */ + function LayerSelection(layer) { + if (!layer) { + throw new Error('Layer is required'); + } + this.layer_ = layer; + } + + LayerSelection.prototype = { + __proto__: Selection.prototype, + + get specicifity() { + return 1; + }, + + get associatedLayerId() { + return this.layer_.layerId; + }, + + get associatedRenderPassId() { + return undefined; + }, + + get layer() { + return this.layer_; + }, + + createAnalysis() { + const dataView = document.createElement( + 'tr-ui-a-generic-object-view-with-label'); + dataView.label = 'Layer ' + this.layer_.layerId; + if (this.layer_.usingGpuRasterization) { + dataView.label += ' (GPU-rasterized)'; + } + dataView.object = this.layer_.args; + return dataView; + }, + + get title() { + return this.layer_.objectInstance.typeName; + }, + + findEquivalent(lthi) { + const layer = lthi.activeTree.findLayerWithId(this.layer_.layerId) || + lthi.pendingTree.findLayerWithId(this.layer_.layerId); + if (!layer) return undefined; + return new LayerSelection(layer); + } + }; + + /** + * @constructor + */ + function TileSelection(tile, opt_data) { + this.tile_ = tile; + this.data_ = opt_data || {}; + } + + TileSelection.prototype = { + __proto__: Selection.prototype, + + get specicifity() { + return 2; + }, + + get associatedLayerId() { + return this.tile_.layerId; + }, + + get highlightsByLayerId() { + const highlights = {}; + highlights[this.tile_.layerId] = [ + { + colorKey: this.tile_.objectInstance.typeName, + rect: this.tile_.layerRect + } + ]; + return highlights; + }, + + createAnalysis() { + const analysis = document.createElement( + 'tr-ui-a-generic-object-view-with-label'); + analysis.label = 'Tile ' + this.tile_.objectInstance.id + ' on layer ' + + this.tile_.layerId; + if (this.data_) { + analysis.object = { + moreInfo: this.data_, + tileArgs: this.tile_.args + }; + } else { + analysis.object = this.tile_.args; + } + return analysis; + }, + + findEquivalent(lthi) { + const tileInstance = this.tile_.tileInstance; + if (lthi.ts < tileInstance.creationTs || + lthi.ts >= tileInstance.deletionTs) { + return undefined; + } + const tileSnapshot = tileInstance.getSnapshotAt(lthi.ts); + if (!tileSnapshot) return undefined; + return new TileSelection(tileSnapshot); + } + }; + + /** + * @constructor + */ + function LayerRectSelection(layer, rectType, rect, opt_data) { + this.layer_ = layer; + this.rectType_ = rectType; + this.rect_ = rect; + this.data_ = opt_data !== undefined ? opt_data : rect; + } + + LayerRectSelection.prototype = { + __proto__: Selection.prototype, + + get specicifity() { + return 2; + }, + + get associatedLayerId() { + return this.layer_.layerId; + }, + + + get highlightsByLayerId() { + const highlights = {}; + highlights[this.layer_.layerId] = [ + { + colorKey: this.rectType_, + rect: this.rect_ + } + ]; + return highlights; + }, + + createAnalysis() { + const analysis = document.createElement( + 'tr-ui-a-generic-object-view-with-label'); + analysis.label = this.rectType_ + ' on layer ' + this.layer_.layerId; + analysis.object = this.data_; + return analysis; + }, + + findEquivalent(lthi) { + return undefined; + } + }; + + /** + * @constructor + */ + function AnimationRectSelection(layer, rect) { + this.layer_ = layer; + this.rect_ = rect; + } + + AnimationRectSelection.prototype = { + __proto__: Selection.prototype, + + get specicifity() { + return 0; + }, + + get associatedLayerId() { + return this.layer_.layerId; + }, + + createAnalysis() { + const analysis = document.createElement( + 'tr-ui-a-generic-object-view-with-label'); + analysis.label = 'Animation Bounds of layer ' + this.layer_.layerId; + analysis.object = this.rect_; + return analysis; + } + }; + + return { + Selection, + RenderPassSelection, + LayerSelection, + TileSelection, + LayerRectSelection, + AnimationRectSelection, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html new file mode 100644 index 00000000000..ad1f633f334 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/chrome/cc/tile.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> + +<script> + +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + /* + * Displays a tile in a human readable form. + * @constructor + */ + const TileSnapshotView = tr.ui.b.define( + 'tr-ui-e-chrome-cc-tile-snapshot-view', + tr.ui.analysis.ObjectSnapshotView); + + TileSnapshotView.prototype = { + __proto__: tr.ui.analysis.ObjectSnapshotView.prototype, + + decorate() { + Polymer.dom(this).classList.add('tr-ui-e-chrome-cc-tile-snapshot-view'); + this.layerTreeView_ = + new tr.ui.e.chrome.cc.LayerTreeHostImplSnapshotView(); + Polymer.dom(this).appendChild(this.layerTreeView_); + }, + + updateContents() { + const tile = this.objectSnapshot_; + const layerTreeHostImpl = tile.containingSnapshot; + if (!layerTreeHostImpl) return; + + this.layerTreeView_.objectSnapshot = layerTreeHostImpl; + this.layerTreeView_.selection = new tr.ui.e.chrome.cc.TileSelection(tile); + } + }; + + tr.ui.analysis.ObjectSnapshotView.register( + TileSnapshotView, + { + typeName: 'cc::Tile', + showInTrackView: false + }); + + return { + TileSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html new file mode 100644 index 00000000000..af6c79447bc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +<dom-module id='tr-ui-e-chrome-codesearch'> + <template> + <style> + :host { + white-space: nowrap; + } + #codesearchLink { + font-size: x-small; + margin-left: 20px; + text-decoration: none; + } + </style> + <a id="codesearchLink" target=_blank on-click="onClick">🔍</a> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome', function() { + Polymer({ + is: 'tr-ui-e-chrome-codesearch', + + set searchPhrase(phrase) { + const link = Polymer.dom(this.$.codesearchLink); + const codeSearchURL = + 'https://cs.chromium.org/search/?sq=package:chromium&type=cs&q='; + link.setAttribute('href', codeSearchURL + encodeURIComponent(phrase)); + }, + + onClick(clickEvent) { + // Let the event trigger the default action of following the link. Stop + // the propagation of the event here, so that subsequent handlers do not + // intercept the clicks. + clickEvent.stopPropagation(); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html new file mode 100644 index 00000000000..ec7991b6640 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/chrome/gpu/gpu_async_slice.html"> +<link rel="import" href="/tracing/extras/chrome/gpu/state.html"> +<link rel="import" href="/tracing/ui/extras/chrome/gpu/state_view.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.png b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.png Binary files differnew file mode 100644 index 00000000000..8ea9bc726bb --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.png diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css new file mode 100644 index 00000000000..7c2c34787dc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css @@ -0,0 +1,15 @@ +/* Copyright (c) 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +.tr-ui-e-chrome-gpu-state-snapshot-view { + background: url('./images/checkerboard.png'); + display: flex; + overflow: auto; +} + +.tr-ui-e-chrome-gpu-state-snapshot-view img { + display: block; + margin: 16px auto 16px auto; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html new file mode 100644 index 00000000000..ba6c345be5f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="stylesheet" href="/tracing/ui/extras/chrome/gpu/state_view.css"> + +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.gpu', function() { + /* + * Displays a GPU state snapshot in a human readable form. + * @constructor + */ + const StateSnapshotView = tr.ui.b.define( + 'tr-ui-e-chrome-gpu-state-snapshot-view', + tr.ui.analysis.ObjectSnapshotView); + + StateSnapshotView.prototype = { + __proto__: tr.ui.analysis.ObjectSnapshotView.prototype, + + decorate() { + Polymer.dom(this).classList.add('tr-ui-e-chrome-gpu-state-snapshot-view'); + this.screenshotImage_ = document.createElement('img'); + Polymer.dom(this).appendChild(this.screenshotImage_); + }, + + updateContents() { + if (this.objectSnapshot_ && this.objectSnapshot_.screenshot) { + this.screenshotImage_.src = 'data:image/png;base64,' + + this.objectSnapshot_.screenshot; + } + } + }; + tr.ui.analysis.ObjectSnapshotView.register( + StateSnapshotView, + {typeName: 'gpu::State'}); + + return { + StateSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html new file mode 100644 index 00000000000..db80ef75afa --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html @@ -0,0 +1,229 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/chrome/layout_tree.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> + +<dom-module id='tr-ui-a-layout-tree-sub-view'> + <template> + <style> + tr-ui-b-table { + font-size: 12px; + } + </style> + <div id="content"></div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + Polymer({ + is: 'tr-ui-a-layout-tree-sub-view', + behaviors: ['tr-ui-a-sub-view'], + + set selection(selection) { + this.currentSelection_ = selection; + this.updateContents_(); + }, + + get selection() { + return this.currentSelection_; + }, + + updateContents_() { + this.set('$.content.textContent', ''); + if (!this.currentSelection_) return; + + const columns = [ + { + title: 'Tag/Name', + value(layoutObject) { + return layoutObject.tag || ':' + layoutObject.name; + } + }, + + { + title: 'htmlId', + value(layoutObject) { + return layoutObject.htmlId || ''; + } + }, + + { + title: 'classNames', + value(layoutObject) { + return layoutObject.classNames || ''; + } + }, + + { + title: 'reasons', + value(layoutObject) { + return layoutObject.needsLayoutReasons.join(', '); + } + }, + + { + title: 'width', + value(layoutObject) { + return layoutObject.absoluteRect.width; + } + }, + + { + title: 'height', + value(layoutObject) { + return layoutObject.absoluteRect.height; + } + }, + + { + title: 'absX', + value(layoutObject) { + return layoutObject.absoluteRect.left; + } + }, + + { + title: 'absY', + value(layoutObject) { + return layoutObject.absoluteRect.top; + } + }, + + { + title: 'relX', + value(layoutObject) { + return layoutObject.relativeRect.left; + } + }, + + { + title: 'relY', + value(layoutObject) { + return layoutObject.relativeRect.top; + } + }, + + { + title: 'float', + value(layoutObject) { + return layoutObject.isFloat ? 'float' : ''; + } + }, + + { + title: 'positioned', + value(layoutObject) { + return layoutObject.isPositioned ? 'positioned' : ''; + } + }, + + { + title: 'relative', + value(layoutObject) { + return layoutObject.isRelativePositioned ? 'relative' : ''; + } + }, + + { + title: 'sticky', + value(layoutObject) { + return layoutObject.isStickyPositioned ? 'sticky' : ''; + } + }, + + { + title: 'anonymous', + value(layoutObject) { + return layoutObject.isAnonymous ? 'anonymous' : ''; + } + }, + + { + title: 'row', + value(layoutObject) { + if (layoutObject.tableRow === undefined) { + return ''; + } + return layoutObject.tableRow; + } + }, + + { + title: 'col', + value(layoutObject) { + if (layoutObject.tableCol === undefined) { + return ''; + } + return layoutObject.tableCol; + } + }, + + { + title: 'rowSpan', + value(layoutObject) { + if (layoutObject.tableRowSpan === undefined) { + return ''; + } + return layoutObject.tableRowSpan; + } + }, + + { + title: 'colSpan', + value(layoutObject) { + if (layoutObject.tableColSpan === undefined) { + return ''; + } + return layoutObject.tableColSpan; + } + }, + + { + title: 'address', + value(layoutObject) { + return layoutObject.id.toString(16); + } + } + ]; + + const table = this.ownerDocument.createElement('tr-ui-b-table'); + table.defaultExpansionStateCallback = function( + layoutObject, parentLayoutObject) { + return true; + }; + table.subRowsPropertyName = 'childLayoutObjects'; + table.tableColumns = columns; + table.tableRows = this.currentSelection_.map(function(snapshot) { + return snapshot.rootLayoutObject; + }); + table.rebuild(); + Polymer.dom(this.$.content).appendChild(table); + }, + }); + + return {}; +}); +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-layout-tree-sub-view', + tr.e.chrome.LayoutTreeSnapshot, + { + multi: false, + title: 'Layout Tree', + }); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-layout-tree-sub-view', + tr.e.chrome.LayoutTreeSnapshot, + { + multi: true, + title: 'Layout Trees', + }); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html new file mode 100644 index 00000000000..cea42a2d78c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<!-- +The chrome config is heavily used: + - chrome://tracing, + - trace2html, which in turn implies + - adb_profile_chrome + - telemetry +--> + +<!-- +TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the +same way that all non-UI files depend on tracing/base/base.html. Enforce this +dependency with a presubmit. +--> +<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order> + +<link rel="import" href="/tracing/extras/chrome_config.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/cc.html"> +<link rel="import" href="/tracing/ui/extras/chrome/codesearch.html"> +<link rel="import" href="/tracing/ui/extras/chrome/gpu/gpu.html"> +<link rel="import" href="/tracing/ui/extras/chrome/layout_tree_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/side_panel/frame_data_side_panel.html"> +<link rel="import" href="/tracing/ui/extras/side_panel/input_latency_side_panel.html"> +<link rel="import" href="/tracing/ui/extras/system_stats/system_stats.html"> +<link rel="import" href="/tracing/ui/extras/v8_config.html"> +<link rel="import" href="/tracing/ui/timeline_view.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html new file mode 100644 index 00000000000..cb3d41a88a0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html @@ -0,0 +1,123 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/table.html"> + +<!-- +This class tries to (simply) copy the telemetry Results object, but outputs +directly to an HTML table. It takes things that look like Telemetry values, +and updates the table internally. +--> +<dom-module id='tr-ui-e-deep-reports-html-results'> + <template> + <style> + :host { + display: flex; + font-size: 12px; + } + </style> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> + +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-deep-reports-html-results', + + created() { + this.hasColumnNamed_ = {}; + this.pageToRowMap_ = new WeakMap(); + }, + + ready() { + const table = this.$.table; + table.tableColumns = [ + { + title: 'Label', + value(row) { return row.label; }, + width: '350px' + } + ]; + this.clear(); + }, + + clear() { + this.$.table.tableRows = []; + }, + + addColumnIfNeeded_(columnName) { + if (this.hasColumnNamed_[columnName]) return; + + this.hasColumnNamed_[columnName] = true; + + const column = { + title: columnName, + value(row) { + if (row[columnName] === undefined) return ''; + return row[columnName]; + } + }; + + const columns = this.$.table.tableColumns; + columns.push(column); + + // Update widths. + let colWidthPercentage; + if (columns.length === 1) { + colWidthPercentage = '100%'; + } else { + colWidthPercentage = (100 / (columns.length - 1)).toFixed(3) + '%'; + } + + for (let i = 1; i < columns.length; i++) { + columns[i].width = colWidthPercentage; + } + + this.$.table.tableColumns = columns; + }, + + getRowForPage_(page) { + if (!this.pageToRowMap_.has(page)) { + const i = page.url.lastIndexOf('/'); + const baseName = page.url.substring(i + 1); + + const link = document.createElement('a'); + link.href = 'trace_viewer.html#' + page.url; + Polymer.dom(link).textContent = baseName; + + const row = { + label: link, + value: '', + subRows: [], + isExpanded: true + }; + this.$.table.tableRows.push(row); + this.pageToRowMap_.set(page, row); + + // Kick table rebuild. + this.$.table.tableRows = this.$.table.tableRows; + } + return this.pageToRowMap_.get(page); + }, + + addValue(value) { + /* Value is expected to be a scalar telemetry-style Value. */ + if (value.type !== 'scalar') { + throw new Error('wat'); + } + + this.addColumnIfNeeded_(value.name); + const rowForPage = this.getRowForPage_(value.page); + rowForPage[value.name] = value.value; + + // Kick table rebuild. + this.$.table.tableRows = this.$.table.tableRows; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html new file mode 100644 index 00000000000..9cf2d7f6c79 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/base/xhr.html"> +<link rel="import" href="/tracing/ui/extras/deep_reports/scalar_value.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.deep_reports', function() { + /** + * Runs deep reports on the provided files, and pushes telemetry-style + * values to the results object. + */ + function main(results, filesInDir) { + let lastP = new Promise(function(resolve) { resolve(); }); + + filesInDir.forEach(function(filename) { + // TODO(nduca): Make this like telemetry page. + const page = { + url: filename + }; + lastP = lastP.then(function() { + return loadModelFromFileAsync(filename); + }); + lastP = lastP.then(function(model) { + processModel(results, page, model); + }); + }); + return lastP; + } + + function loadModelFromFileAsync(filename) { + return tr.b.getAsync(filename).then(function(trace) { + const io = new tr.ImportOptions(); + io.shiftWorldToZero = true; + io.pruneEmptyContainers = false; + + const m = new tr.Model(); + try { + m.importTraces([trace], io); + } catch (e) { + throw new Error('While loading ' + filename + ' got: ' + e.toString()); + } + return m; + }); + } + + function processModel(results, page, model) { + results.addValue( + new tr.ui.e.deep_reports.ScalarValue( + page, 'numRailIRs', 'ms', model.userModel.expectations.length)); + } + + return { + main + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html new file mode 100644 index 00000000000..cb550c35b4b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.deep_reports', function() { + function ScalarValue(page, name, units, value, + opt_important, opt_description) { + this.type = 'scalar'; + this.page = page; + this.name = name; + this.units = units; + this.value = value; + this.important = opt_important !== undefined ? opt_important : false; + this.description = opt_description || ''; + } + ScalarValue.fromDict = function(page, dict) { + if (dict.type !== 'scalar') { + throw new Error('wat'); + } + const v = new ScalarValue(page, dict.name, dict.units, dict.value); + v.important = dict.important; + v.description = dict.description; + v.value = dict.value; + return v; + }; + + ScalarValue.prototype = { + + }; + + return { + ScalarValue, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html new file mode 100644 index 00000000000..1748dd12b88 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<dom-module id='tr-ui-e-drive-comment-element'> + <template> + <style> + :host { + display: block; + } + #comment-area { + display: flex; + flex-direction: column; + border-top: 1px solid #e8e8e8; + background-color: white; + padding: 6px; + margin-bottom: 4px; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); + border-radius: 2px; + font-size: small; + } + #comment-header { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 8px; + } + #comment-header-text { + display: flex; + flex-direction: column; + padding-left: 10px; + } + #comment-img { + width: 32px; + height: 32px; + } + #comment-text-author { + padding-bottom: 2px; + } + #comment-date { + color: #777; + font-size: 11px; + } + #comment-content { + word-wrap: break-word; + } + </style> + <div id="comment-area"> + <div id="comment-header"> + <img id="comment-img" src="{{ comment.author.picture.url }}" /> + <div id="comment-header-text"> + <div id="comment-text-author">{{ comment.author.displayName }}</div> + <div id="comment-date">{{ createdDate }}</div> + </div> + </div> + <div id="comment-content">{{_computeCommentContentPrefix( comment)}} + {{ comment.content }}</div> + </div> + </template> +</dom-module> +<script> +'use strict'; +Polymer({ + is: 'tr-ui-e-drive-comment-element', + + properties: { + comment: { + type: String, + observer: '_commentChanged' + } + }, + + _commentChanged() { + this.createdDate = new Date(this.comment.createdDate).toLocaleString(); + }, + + _computeCommentContentPrefix(comment) { + return comment.anchor ? '⚓ ' : ''; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html new file mode 100644 index 00000000000..8f52839029d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html @@ -0,0 +1,185 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/extras/drive/comment_element.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html"> + +<dom-module id='tr-ui-e-drive-comments-side-panel'> + <template> + <style> + :host { + flex-direction: column; + display: flex; + width: 290px; + overflow-y: scroll; + overflow-x: hidden; + background-color: #eee; + } + toolbar { + flex: 0 0 auto; + border-bottom: 1px solid black; + display: flex; + } + result-area { + flex: 1 1 auto; + display: block; + min-height: 0; + padding: 4px; + } + #comments-textarea-container { + display: flex; + } + #commentinput { + width: 100%; + } + </style> + + <toolbar id='toolbar'></toolbar> + <result-area id='result_area'> + <template is="dom-repeat" items="{{comments_}}" repeat="{{ comment in comments_ }}"> + <tr-ui-e-drive-comment-element comment="{{comment}}" + on-click="commentClick"> + </tr-ui-e-drive-comment-element> + </template> + <div id="comments-textarea-container"> + <textarea id="commentinput" on-focus='textAreaFocus' + on-blur='textAreaBlur' + on-keypress="textareaKeypress"></textarea> + </div> + </result-area> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-drive-comments-side-panel', + behaviors: [tr.ui.behaviors.SidePanel], + + ready() { + this.rangeOfInterest_ = new tr.b.math.Range(); + this.selection_ = undefined; + this.comments_ = []; + this.annotationFromComment_ = undefined; + this.textAreaFocused = false; + }, + + setCommentProvider(commentProvider) { + this.commentProvider_ = commentProvider; + }, + + attached() { + if (this.commentProvider_ === undefined) { + this.commentProvider_ = + new tr.ui.e.drive.analysis.DefaultCommentProvider(); + } + this.commentProvider_.attachToElement(this); + }, + + detached() { + this.commentProvider_.detachFromElement(); + }, + + commentClick(event) { + const anchor = event.currentTarget.comment.anchor; + if (!anchor) return; + + const uiState = + JSON.parse(anchor).a[0][tr.ui.e.drive.constants.ANCHOR_NAME]; + + const myEvent = new CustomEvent('navigateToUIState', { detail: + new tr.ui.b.UIState(new tr.model.Location(uiState.location.xWorld, + uiState.location.yComponents), + uiState.scaleX) + }); + document.dispatchEvent(myEvent); + + if (this.annotationFromComment_) { + this.model.removeAnnotation(this.annotationFromComment_); + } + const loc = new tr.model.Location(uiState.location.xWorld, + uiState.location.yComponents); + + const text = sender.comment.author.displayName + ': ' + + sender.comment.content; + this.annotationFromComment_ = + new tr.model.CommentBoxAnnotation(loc, text); + this.model.addAnnotation(this.annotationFromComment_); + }, + + textareaKeypress(event) { + // Check for return key. + if (event.keyCode === 13 && !event.ctrlKey) { + this.commentProvider_.addComment(this.$.commentinput.value); + this.$.commentinput.value = ''; + } + event.stopPropagation(); + return true; + }, + + textAreaFocus(event) { + this.textAreaFocused = true; + }, + + textAreaBlur(event) { + this.textAreaFocused = false; + }, + + get rangeOfInterest() { + return this.rangeOfInterest_; + }, + + set rangeOfInterest(rangeOfInterest) { + this.rangeOfInterest_ = rangeOfInterest; + this.updateContents_(); + }, + + get currentRangeOfInterest() { + if (this.rangeOfInterest_.isEmpty) { + return this.model_.bounds; + } + return this.rangeOfInterest_; + }, + + get model() { + return this.model_; + }, + + set model(model) { + this.model_ = model; + this.updateContents_(); + }, + + set selection(selection) { + this.selection_ = selection; + }, + + updateContents_() { + this.commentProvider_.updateComments(); + }, + + supportsModel(m) { + if (m === undefined) { + return { + supported: false, + reason: 'Unknown tracing model' + }; + } + return { + supported: true + }; + }, + + get textLabel() { + return 'Comments'; + } +}); + +tr.ui.side_panel.SidePanelRegistry.register(function() { + return document.createElement('tr-ui-e-drive-comments-side-panel'); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html new file mode 100644 index 00000000000..639c1b9b597 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/extras/drive/comments_side_panel.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function StubCommentProvider() { + this.addDummyComment('Lorem ipsum dolor sit amet'); + this.addDummyComment('consectetur adipiscing elit'); + this.addDummyComment('sed do eiusmod tempor incididunt ut labore et ' + + 'dolore magna aliqua. Ut enim ad minim veniam, quis nostrud ' + + 'exercitation ullamco laboris nisi ut aliquip ex ea commodo ' + + 'consequat. Duis aute irure dolor in reprehenderit in voluptate ' + + 'velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint ' + + 'occaecat cupidatat non proident, sunt in culpa qui officia deserunt ' + + 'mollit anim id est laborum.'); + } + + StubCommentProvider.prototype = { + comments_: [], + + attachToElement(attachedElement) { + this.attachedElement_ = attachedElement; + this.updateComments(); + }, + + detachFromElement() { + }, + + updateComments() { + this.attachedElement_.comments_ = this.comments_; + }, + + addDummyComment(content) { + const newComment = { + author: { + displayName: 'Casper the Friendly Ghost', + picture: { + url: 'https://lh3.googleusercontent.com/-XdUIqdMkCWA/' + + 'AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/s128/photo.jpg' + } + }, + createdDate: Date.now(), + anchor: (this.comments_.length) % 2 ? 1 : 0, + content + }; + + this.comments_.push(newComment); + }, + + addComment(body) { + this.addDummyComment(body); + this.updateComments(); + } + }; + + test('instantiate', function() { + const panel = document.createElement('tr-ui-e-drive-comments-side-panel'); + panel.setCommentProvider(new StubCommentProvider); + this.addHTMLOutput(panel); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html new file mode 100644 index 00000000000..42d3706f854 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html @@ -0,0 +1,99 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/comment_box_annotation.html"> + +<link rel="import" href="/tracing/ui/extras/drive/comments_side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel.html"> + +<script> +'use strict'; + +(function() { + function addDriveCommentWithUIState_(text, uiState) { + gapi.client.load('drive', 'v2', function() { + const request = gapi.client.drive.revisions.get({ + 'fileId': tr.ui.e.drive.getDriveFileId(), + 'revisionId': 'head' + }); + request.execute(function(resp) { + const anchorObject = {}; + anchorObject[tr.ui.e.drive.constants.ANCHOR_NAME] = uiState; + let anchor = { + 'r': resp.id, + 'a': [anchorObject] + }; + anchor = JSON.stringify(anchor); + gapi.client.load('drive', 'v2', function() { + const request = gapi.client.drive.comments.insert({ + 'fileId': tr.ui.e.drive.getDriveFileId(), + 'resource': {'content': text, anchor} + }); + request.execute(); + }); + }); + }); + } + + function onCommentWithUIState(e) { + addDriveCommentWithUIState_(e.detail.name, e.detail.location); + } + + document.addEventListener('commentWithUIState', + onCommentWithUIState.bind(this)); +}()); + +tr.exportTo('tr.ui.e.drive.analysis', function() { + function DefaultCommentProvider() { } + + DefaultCommentProvider.prototype = { + attachToElement(attachedElement) { + this.attachedElement_ = attachedElement; + this.commentsCheckTimer_ = setTimeout(this.checkForComments_.bind(this), + 5000); + }, + + detachFromElement() { + clearTimeout(this.commentsCheckTimer_); + }, + + checkForComments_() { + this.updateComments(); + this.commentsCheckTimer_ = setTimeout(this.checkForComments_.bind(this), + 5000); + }, + + updateComments() { + gapi.client.load('drive', 'v2', () => { + const request = gapi.client.drive.comments.list({ + 'fileId': tr.ui.e.drive.getDriveFileId() + }); + request.execute(results => { + this.attachedElement_.comments_ = results.items; + }); + }); + }, + + addComment(body) { + gapi.client.load('drive', 'v2', () => { + const request = gapi.client.drive.comments.insert({ + 'fileId': tr.ui.e.drive.getDriveFileId(), + 'resource': {'content': body} + }); + request.execute(resp => { + this.updateComments(); + }); + }); + } + }; + + return { + DefaultCommentProvider, + }; +}); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html new file mode 100644 index 00000000000..270dcccf2fc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html @@ -0,0 +1,463 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + + <script type="text/javascript" src="https://apis.google.com/js/api.js"></script> + + <link rel="import" href="/components/polymer/polymer.html"> + <link rel="import" href="/tracing/ui/extras/drive/drive_comment_provider.html"> + <link rel="import" href="/tracing/ui/extras/full_config.html"> + <link rel="import" href="/tracing/ui/timeline_view.html"> + + <style> + body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + } + body > x-timeline-view { + flex: 1 1 auto; + overflow: hidden; + position: absolute; + top: 0px; + bottom: 0; + left: 0; + right: 0; + } + body > x-timeline-view:focus { + outline: none; + } + nav { + display: flex; + flex-direction: row; + justify-content: flex-end; + } + #navbar button { + height: 24px; + padding-bottom: 3px; + vertical-align: middle; + box-shadow: none; + background-color: #4d90fe; + background-image: -webkit-linear-gradient(top,#4d90fe,#4787ed); + border: 1px solid #3079ed; + color: #fff; + border-radius: 2px; + cursor: default; + font-size: 11px; + font-weight: bold; + text-align: center; + white-space: nowrap; + line-height: 27px; + min-width: 54px; + outline: 0px; + padding: 0 8px; + font: normal 13px arial,sans-serif; + margin: 5px; + } + #collabs { + display: flex; + flex-direction: row; + } + .collaborator-div { + display: inline-block; + vertical-align: middle; + min-height: 0; + width: 100px; + font-size: 11px; + font-weight: bold; + font: normal 13px arial,sans-serif; + margin: 10px; + } + .collaborator-img { + margin: 2px; + } + .collaborator-tooltip { + z-index: 10000; + transition: visibility 0,opacity .13s ease-in; + background-color: #2a2a2a; + border: 1px solid #fff; + color: #fff; + cursor: default; + display: block; + font-family: arial, sans-serif; + font-size: 11px; + font-weight: bold; + margin-left: -1px; + opacity: 1; + padding: 7px 9px; + word-break: break-word; + position: absolute; + } + .collaborator-tooltip-content { + color: #fff; + } + .collaborator-tooltip-arrow { + position: absolute; + top: -6px; + } + .collaborator-tooltip-arrow-before { + border-color: #fff transparent !important; + left: -6px; + border: 6px solid; + border-top-width: 0; + content: ''; + display: block; + height: 0; + position: absolute; + width: 0; + } + .collaborator-tooltip-arrow-after { + top: 1px; + border-color: #2a2a2a transparent !important; + left: -5px; + border: 5px solid; + border-top-width: 0; + content: ''; + display: block; + height: 0; + position: absolute; + width: 0; + } + + </style> + <title>Trace Viewer</title> +</head> +<body> + <nav id="navbar"> + <div id="collabs"></div> + <button id="x-drive-save-to-disk">Save to disk</button> + <button id="x-drive-save-to-drive">Save to Drive</button> + <button id="x-drive-load-from-drive">Load from Drive</button> + <button id="x-drive-share">Share</button> + </nav> + <x-timeline-view> + </x-timeline-view> + + <script> + 'use strict'; + + // Needs to be global as it's passed through the Google API as a + // GET parameter. + let onAPIClientLoaded_ = null; + + (function() { + tr.exportTo('tr.ui.e.drive', function() { + const appId = '239864068844'; + const constants = { + APP_ID: appId, + ANCHOR_NAME: appId + '.trace_viewer', + DEVELOPER_KEY: 'AIzaSyDR-6_wL9vHg1_oz4JHk8IQAkv2_Y0Y8-M', + CLIENT_ID: '239864068844-c7gefbfdcp0j6grltulh2r88tsvl18c1.apps.' + + 'googleusercontent.com', + SCOPE: [ + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/drive.install', + 'https://www.googleapis.com/auth/drive.file', + 'profile' + ] + }; + + return { + getDriveFileId() { return driveFileId_; }, + constants + }; + }); + + + let pickerApiLoaded_ = false; + let oauthToken_ = null; + + let timelineViewEl_ = null; + let driveDocument_ = null; + let shareClient_ = null; + let fileIdToLoad_ = null; + let driveFileId_ = null; + + function parseGETParameter(val) { + let result = null; + let tmp = []; + location.search.substr(1).split('&').forEach(function(item) { + tmp = item.split('='); + if (tmp[0] === val) { + result = decodeURIComponent(tmp[1]); + } + }); + return result; + } + + // Use the Google API Loader script to load the google.picker script. + onAPIClientLoaded_ = function() { + const driveState = parseGETParameter('state'); + if (driveState !== null) { + const driveStateJson = JSON.parse(driveState); + fileIdToLoad_ = String(driveStateJson.ids); + } + + gapi.load('picker', {'callback': onPickerApiLoad}); + gapi.load('auth', {'callback'() { + onAuthApiLoad(true, onAuthResultSuccess); + return tr.b.timeout(30e3) + .then(() => onAuthApiLoad(true, function() {})) + .then(() => tr.b.timeout(30e3)) + .then(() => onRepeatAuthApiLoad); + }}); + }; + + function onAuthApiLoad(tryImmediate, resultCallback) { + window.gapi.auth.authorize( + {'client_id': tr.ui.e.drive.constants.CLIENT_ID, + 'scope': tr.ui.e.drive.constants.SCOPE, 'immediate': tryImmediate}, + function(authResult) { + handleAuthResult(authResult, tryImmediate, resultCallback); + }); + } + + function onPickerApiLoad() { + pickerApiLoaded_ = true; + if (fileIdToLoad_ === null) { + createPicker(); + } + } + + function onAuthResultSuccess() { + if (fileIdToLoad_ === null) { + createPicker(); + } else { + loadFileFromDrive(fileIdToLoad_); + } + } + + function handleAuthResult(authResult, wasImmediate, resultCallback) { + if (authResult && !authResult.error) { + oauthToken_ = authResult.access_token; + resultCallback(); + } else if (wasImmediate) { + onAuthApiLoad(false); + } + } + + function createPicker() { + if (pickerApiLoaded_ && oauthToken_) { + const view = new google.picker.View(google.picker.ViewId.DOCS); + view.setMimeTypes('application/json,application/octet-stream'); + const picker = new google.picker.PickerBuilder() + .enableFeature(google.picker.Feature.NAV_HIDDEN) + .enableFeature(google.picker.Feature.MULTISELECT_ENABLED) + .setAppId(tr.ui.e.drive.constants.APP_ID) + .setOAuthToken(oauthToken_) + .addView(view) + .addView(new google.picker.DocsUploadView()) + .setDeveloperKey(tr.ui.e.drive.constants.DEVELOPER_KEY) + .setCallback(pickerCallback) + .build(); + picker.setVisible(true); + } + } + + function pickerCallback(data) { + if (data.action === google.picker.Action.PICKED) { + loadFileFromDrive(data.docs[0].id); + } + } + + function initShareButton() { + shareClient_ = new gapi.drive.share.ShareClient( + tr.ui.e.drive.constants.APP_ID); + shareClient_.setItemIds([driveFileId_]); + } + + function loadFileFromDrive(fileId) { + gapi.client.load('drive', 'v2', function() { + const request = gapi.client.drive.files.get({fileId}); + request.execute(function(resp) { downloadFile(resp); }); + driveFileId_ = fileId; + gapi.load('drive-share', initShareButton); + }); + } + + function downloadFile(file) { + if (file.downloadUrl) { + const downloadingOverlay = tr.ui.b.Overlay(); + downloadingOverlay.title = 'Downloading...'; + downloadingOverlay.userCanClose = false; + downloadingOverlay.msgEl = document.createElement('div'); + Polymer.dom(downloadingOverlay).appendChild(downloadingOverlay.msgEl); + downloadingOverlay.msgEl.style.margin = '20px'; + downloadingOverlay.update = function(msg) { + Polymer.dom(this.msgEl).textContent = msg; + }; + downloadingOverlay.visible = true; + + const accessToken = gapi.auth.getToken().access_token; + const xhr = new XMLHttpRequest(); + xhr.open('GET', file.downloadUrl); + xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken); + xhr.onload = function() { + downloadingOverlay.visible = false; + onDownloaded(file.title, xhr.responseText); + }; + xhr.onprogress = function(evt) { + downloadingOverlay.update( + Math.floor(evt.position * 100 / file.fileSize) + '% complete'); + }; + xhr.onerror = function() { alert('Failed downloading!'); }; + xhr.send(); + } else { + alert('No URL!'); + } + } + + function displayAllCollaborators() { + const allCollaborators = driveDocument_.getCollaborators(); + const collaboratorCount = allCollaborators.length; + const collabspan = document.getElementById('collabs'); + Polymer.dom(collabspan).innerHTML = ''; + const imageList = []; + for (let i = 0; i < collaboratorCount; i++) { + const user = allCollaborators[i]; + + const img = document.createElement('img'); + img.src = user.photoUrl; + img.alt = user.displayName; + img.height = 30; + img.width = 30; + img.className = 'collaborator-img'; + Polymer.dom(collabspan).appendChild(img); + imageList.push({'image': img, 'name': user.displayName}); + } + for (i = 0; i < imageList.length; i++) { + const collabTooltip = tr.ui.b.createDiv({ + className: 'collaborator-tooltip' + }); + const collabTooltipContent = tr.ui.b.createDiv({ + className: 'collaborator-tooltip-content' + }); + Polymer.dom(collabTooltipContent).textContent = imageList[i].name; + Polymer.dom(collabTooltip).appendChild(collabTooltipContent); + Polymer.dom(collabspan).appendChild(collabTooltip); + const collabTooltipArrow = tr.ui.b.createDiv({ + className: 'collaborator-tooltip-arrow'}); + Polymer.dom(collabTooltip).appendChild(collabTooltipArrow); + const collabTooltipArrowBefore = tr.ui.b.createDiv({ + className: 'collaborator-tooltip-arrow-before'}); + Polymer.dom(collabTooltipArrow).appendChild(collabTooltipArrowBefore); + const collabTooltipArrowAfter = tr.ui.b.createDiv({ + className: 'collaborator-tooltip-arrow-after'}); + Polymer.dom(collabTooltipArrow).appendChild(collabTooltipArrowAfter); + + const rect = imageList[i].image.getBoundingClientRect(); + collabTooltip.style.top = (rect.bottom - 6) + 'px'; + collabTooltip.style.left = + (rect.left + 16 - (collabTooltip.offsetWidth / 2)) + 'px'; + collabTooltipArrow.style.left = (collabTooltip.offsetWidth / 2) + 'px'; + collabTooltip.style.visibility = 'hidden'; + function visibilityDelegate(element, visibility) { + return function() { + element.style.visibility = visibility; + }; + } + imageList[i].image.addEventListener( + 'mouseover', visibilityDelegate(collabTooltip, 'visible')); + imageList[i].image.addEventListener( + 'mouseout', visibilityDelegate(collabTooltip, 'hidden')); + } + } + + function onRealtimeFileLoaded(doc) { + if (driveDocument_) { + driveDocument_.close(); + } + driveDocument_ = doc; + doc.addEventListener(gapi.drive.realtime.EventType.COLLABORATOR_JOINED, + displayAllCollaborators); + doc.addEventListener(gapi.drive.realtime.EventType.COLLABORATOR_LEFT, + displayAllCollaborators); + + displayAllCollaborators(doc); + } + + function onRealtimeError(e) { + alert('Error loading realtime: ' + e); + } + + function onDownloaded(filename, content) { + gapi.load('auth:client,drive-realtime,drive-share', function() { + gapi.drive.realtime.load(driveFileId_, + onRealtimeFileLoaded, + null, + onRealtimeError); + }); + + const traces = []; + const filenames = []; + filenames.push(filename); + traces.push(content); + createViewFromTraces(filenames, traces); + } + + function createViewFromTraces(filenames, traces) { + const m = new tr.Model(); + const i = new tr.importer.Import(m); + const p = i.importTracesWithProgressDialog(traces); + p.then( + function() { + timelineViewEl_.model = m; + timelineViewEl_.updateDocumentFavicon(); + timelineViewEl_.globalMode = true; + timelineViewEl_.viewTitle = ''; + }, + function(err) { + const downloadingOverlay = new tr.ui.b.Overlay(); + Polymer.dom(downloadingOverlay).textContent = + tr.b.normalizeException(err).message; + downloadingOverlay.title = 'Import error'; + downloadingOverlay.visible = true; + }); + } + + function onSaveToDiskClicked() { + throw new Error('Not implemented'); + } + + function onSaveToDriveClicked() { + throw new Error('Not implemented'); + } + + function onLoadFromDriveClicked() { + createPicker(); + } + + function onLoad() { + timelineViewEl_ = Polymer.dom(document).querySelector('x-timeline-view'); + timelineViewEl_.globalMode = true; + const navbar = document.getElementById('navbar'); + timelineViewEl_.style.top = navbar.offsetHeight + 'px'; + tr.ui.b.decorate(timelineViewEl_, tr.ui.TimelineView); + } + + window.addEventListener('load', onLoad); + + document.getElementById('x-drive-save-to-disk').onclick = + onSaveToDiskClicked; + document.getElementById('x-drive-save-to-drive').onclick = + onSaveToDriveClicked; + document.getElementById('x-drive-load-from-drive').onclick = + onLoadFromDriveClicked; + document.getElementById('x-drive-share').onclick = function() { + shareClient_.showSettingsDialog(); + }; + }()); + + </script> + <script type="text/javascript" + src="https://apis.google.com/js/client.js?onload=onAPIClientLoaded_"> + </script> +</body> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html new file mode 100644 index 00000000000..6d1e29d4e20 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<!-- +TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the +same way that all non-UI files depend on tracing/base/base.html. Enforce this +dependency with a presubmit. +--> +<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order> + +<!-- The full config is all the configs slammed together. --> +<link rel="import" href="/tracing/extras/importer/gcloud_trace/gcloud_trace_importer.html"> +<link rel="import" href="/tracing/ui/extras/chrome_config.html"> +<link rel="import" href="/tracing/ui/extras/lean_config.html"> +<link rel="import" href="/tracing/ui/extras/systrace_config.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html new file mode 100644 index 00000000000..8d66352f140 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<!-- +TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the same way that +all non-UI files depend on tracing/base/base.html. Enforce this dependency with a presubmit. +--> +<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order> + +<link rel="import" href="/tracing/extras/lean_config.html" data-suppress-import-order> + +<!-- +The lean config is just enough to import uncompressed, trace-event-formatted +json blobs. +--> +<link rel="import" href="/tracing/ui/side_panel/file_size_stats_side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/metrics_side_panel.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html new file mode 100644 index 00000000000..0971d7f5409 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html @@ -0,0 +1,172 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/line_chart.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html"> + +<dom-module id='tr-ui-e-s-alerts-side-panel'> + <template> + <style> + :host { + display: block; + width: 250px; + } + #content { + flex-direction: column; + display: flex; + } + tr-ui-b-table { + font-size: 12px; + } + </style> + + <div id='content'> + <toolbar id='toolbar'></toolbar> + <result-area id='result_area'></result-area> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-s-alerts-side-panel', + behaviors: [tr.ui.behaviors.SidePanel], + + + ready() { + this.rangeOfInterest_ = new tr.b.math.Range(); + this.selection_ = undefined; + }, + + get model() { + return this.model_; + }, + + set model(model) { + this.model_ = model; + this.updateContents_(); + }, + + set selection(selection) { + }, + + set rangeOfInterest(rangeOfInterest) { + }, + + /** + * Fires a selection event selecting all alerts of the specified + * type. + */ + selectAlertsOfType(alertTypeString) { + const alertsOfType = this.model_.alerts.filter(function(alert) { + return alert.title === alertTypeString; + }); + + const event = new tr.model.RequestSelectionChangeEvent(); + event.selection = new tr.model.EventSet(alertsOfType); + this.dispatchEvent(event); + }, + + /** + * Returns a map for the specified alerts where each key is the + * alert type string and each value is a list of alerts with that + * type. + */ + alertsByType_(alerts) { + const alertsByType = {}; + alerts.forEach(function(alert) { + if (!alertsByType[alert.title]) { + alertsByType[alert.title] = []; + } + + alertsByType[alert.title].push(alert); + }); + return alertsByType; + }, + + alertsTableRows_(alertsByType) { + return Object.keys(alertsByType).map(function(key) { + return { + alertType: key, + count: alertsByType[key].length + }; + }); + }, + + alertsTableColumns_() { + return [ + { + title: 'Alert type', + value(row) { return row.alertType; }, + width: '180px' + }, + { + title: 'Count', + width: '100%', + value(row) { return row.count; } + } + ]; + }, + + createAlertsTable_(alerts) { + const alertsByType = this.alertsByType_(alerts); + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = this.alertsTableColumns_(); + table.tableRows = this.alertsTableRows_(alertsByType); + table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + table.addEventListener('selection-changed', function(e) { + const row = table.selectedTableRow; + if (row) { + this.selectAlertsOfType(row.alertType); + } + }.bind(this)); + + return table; + }, + + updateContents_() { + Polymer.dom(this.$.result_area).textContent = ''; + if (this.model_ === undefined) return; + + const panel = this.createAlertsTable_(this.model_.alerts); + Polymer.dom(this.$.result_area).appendChild(panel); + }, + + supportsModel(m) { + if (m === undefined) { + return { + supported: false, + reason: 'Unknown tracing model' + }; + } else if (m.alerts.length === 0) { + return { + supported: false, + reason: 'No alerts in tracing model' + }; + } + + return { + supported: true + }; + }, + + get textLabel() { + return 'Alerts'; + } +}); + +tr.ui.side_panel.SidePanelRegistry.register(function() { + return document.createElement('tr-ui-e-s-alerts-side-panel'); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html new file mode 100644 index 00000000000..c4cb9825d1e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/side_panel/alerts_side_panel.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ALERT_INFO_1 = new tr.model.EventInfo( + 'Alert 1', 'Critical alert'); + const ALERT_INFO_2 = new tr.model.EventInfo( + 'Alert 2', 'Warning alert'); + + test('instantiate', function() { + const panel = document.createElement('tr-ui-e-s-alerts-side-panel'); + panel.model = createModelWithAlerts([ + new tr.model.Alert(ALERT_INFO_1, 5), + new tr.model.Alert(ALERT_INFO_2, 35) + ]); + panel.style.height = '100px'; + + this.addHTMLOutput(panel); + }); + + test('selectAlertsOfType', function() { + const panel = document.createElement('tr-ui-e-s-alerts-side-panel'); + const alerts = [ + new tr.model.Alert(ALERT_INFO_1, 1), + new tr.model.Alert(ALERT_INFO_1, 2), + new tr.model.Alert(ALERT_INFO_2, 3) + ]; + + const predictedAlerts = new tr.model.EventSet([alerts[0], alerts[1]]); + panel.model = createModelWithAlerts(alerts); + panel.style.height = '100px'; + this.addHTMLOutput(panel); + + let selectionChanged = false; + panel.addEventListener('requestSelectionChange', function(e) { + selectionChanged = true; + assert.isTrue(e.selection.equals(predictedAlerts)); + }); + panel.selectAlertsOfType(ALERT_INFO_1.title); + + assert.isTrue(selectionChanged); + }); + + function createModelWithAlerts(alerts) { + const m = new tr.Model(); + m.alerts = alerts; + return m; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html new file mode 100644 index 00000000000..e5fd7689479 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html @@ -0,0 +1,347 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/extras/chrome/blame_context/frame_tree_node.html"> +<link rel="import" href="/tracing/extras/chrome/blame_context/render_frame.html"> +<link rel="import" href="/tracing/extras/chrome/blame_context/top_level.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-e-s-frame-data-side-panel'> + <template> + <style> + :host { + display: flex; + width: 600px; + flex-direction: column; + } + table-container { + display: flex; + overflow: auto; + font-size: 12px; + } + </style> + <div> + Organize by: + <select id="select"> + <option value="none">None</option> + <option value="tree">Frame Tree</option> + </select> + </div> + <table-container> + <tr-ui-b-table id="table"></tr-ui-b-table> + </table-container> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.ui.e.s', function() { + const BlameContextSnapshot = tr.e.chrome.BlameContextSnapshot; + const FrameTreeNodeSnapshot = tr.e.chrome.FrameTreeNodeSnapshot; + const RenderFrameSnapshot = tr.e.chrome.RenderFrameSnapshot; + const TopLevelSnapshot = tr.e.chrome.TopLevelSnapshot; + + const BlameContextInstance = tr.e.chrome.BlameContextInstance; + const FrameTreeNodeInstance = tr.e.chrome.FrameTreeNodeInstance; + const RenderFrameInstance = tr.e.chrome.RenderFrameInstance; + const TopLevelInstance = tr.e.chrome.TopLevelInstance; + + /** + * @constructor + * If |context| is provided, creates a row for the given context. + * Otherwise, creates an empty Row template which can be used for aggregating + * data from a group of subrows. + */ + function Row(context) { + this.subRows = undefined; + this.contexts = []; + this.type = undefined; + this.renderer = 'N/A'; + this.url = undefined; + this.time = 0; + this.eventsOfInterest = new tr.model.EventSet(); + + if (context === undefined) return; + + this.type = context.objectInstance.blameContextType; + this.contexts.push(context); + if (context instanceof FrameTreeNodeSnapshot) { + if (context.renderFrame) { + this.contexts.push(context.renderFrame); + this.renderer = context.renderFrame.objectInstance.parent.pid; + } + } else if (context instanceof RenderFrameSnapshot) { + if (context.frameTreeNode) { + this.contexts.push(context.frameTreeNode); + } + this.renderer = context.objectInstance.parent.pid; + } else if (context instanceof TopLevelSnapshot) { + this.renderer = context.objectInstance.parent.pid; + } else { + throw new Error('Unknown context type'); + } + this.eventsOfInterest.addEventSet(this.contexts); + + // TODO(xiaochengh): Handle the case where a subframe has a trivial url + // (e.g., about:blank), but inherits the origin of its parent. This is not + // needed now, but will be required if we want to group rows by origin. + this.url = context.url; + } + + const groupFunctions = { + none: rows => rows, + + // Group the rows according to the frame tree structure. + // Example: consider frame tree a(b, c(d)), where each frame has 1ms time + // attributed to it. The resulting table should look like: + // Type | Time | URL + // --------------+------+----- + // Frame Tree | 4 | a + // +- Frame | 1 | a + // +- Subframe | 1 | b + // +- Frame Tree | 2 | c + // +- Frame | 1 | c + // +- Subframe | 1 | d + tree(rows, rowMap) { + // Finds the parent of a specific row. When there is conflict between the + // browser's dump of the frame tree and the renderers', use the browser's. + const getParentRow = function(row) { + let pivot; + row.contexts.forEach(function(context) { + if (context instanceof tr.e.chrome.FrameTreeNodeSnapshot) { + pivot = context; + } + }); + if (pivot && pivot.parentContext) { + return rowMap[pivot.parentContext.guid]; + } + return undefined; + }; + + const rootRows = []; + rows.forEach(function(row) { + const parentRow = getParentRow(row); + if (parentRow === undefined) { + rootRows.push(row); + return; + } + if (parentRow.subRows === undefined) { + parentRow.subRows = []; + } + parentRow.subRows.push(row); + }); + + const aggregateAllDescendants = function(row) { + if (!row.subRows) { + if (getParentRow(row)) { + row.type = 'Subframe'; + } + return row; + } + const result = new Row(); + result.type = 'Frame Tree'; + result.renderer = row.renderer; + result.url = row.url; + result.subRows = [row]; + row.subRows.forEach( + subRow => result.subRows.push(aggregateAllDescendants(subRow))); + result.subRows.forEach(function(subRow) { + result.time += subRow.time; + result.eventsOfInterest.addEventSet(subRow.eventsOfInterest); + }); + row.subRows = undefined; + return result; + }; + + return rootRows.map(rootRow => aggregateAllDescendants(rootRow)); + } + + // TODO(xiaochengh): Add grouping by site and probably more... + }; + + Polymer({ + is: 'tr-ui-e-s-frame-data-side-panel', + behaviors: [tr.ui.behaviors.SidePanel], + + ready() { + this.model_ = undefined; + this.rangeOfInterest_ = new tr.b.math.Range(); + + this.$.table.showHeader = true; + this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + this.$.table.tableColumns = this.createFrameDataTableColumns_(); + + this.$.table.addEventListener('selection-changed', function(e) { + this.selectEventSet_(this.$.table.selectedTableRow.eventsOfInterest); + }.bind(this)); + + this.$.select.addEventListener('change', function(e) { + this.updateContents_(); + }.bind(this)); + }, + + selectEventSet_(eventSet) { + const event = new tr.model.RequestSelectionChangeEvent(); + event.selection = eventSet; + this.dispatchEvent(event); + }, + + createFrameDataTableColumns_() { + return [ + { + title: 'Renderer', + value: row => row.renderer, + cmp: (a, b) => a.renderer - b.renderer + }, + { + title: 'Type', + value: row => row.type + }, + // TODO(xiaochengh): Decide what details to show in the table: + // - URL seems necessary, but we may also want origin instead/both. + // - Distinguish between browser time and renderer time? + // - Distinguish between CPU time and wall clock time? + // - Memory? Network? ... + { + title: 'Time', + value: row => tr.v.ui.createScalarSpan(row.time, { + unit: tr.b.Unit.byName.timeStampInMs, + ownerDocument: this.ownerDocument + }), + cmp: (a, b) => a.time - b.time + }, + { + title: 'URL', + value: row => row.url, + cmp: (a, b) => (a.url || '').localeCompare(b.url || '') + } + ]; + }, + + createFrameDataTableRows_() { + if (!this.model_) return []; + + // Gather contexts into skeletons of rows. + const rows = []; + const rowMap = {}; + for (const proc of Object.values(this.model_.processes)) { + proc.objects.iterObjectInstances(function(objectInstance) { + if (!(objectInstance instanceof BlameContextInstance)) { + return; + } + objectInstance.snapshots.forEach(function(snapshot) { + if (rowMap[snapshot.guid]) return; + + const row = new Row(snapshot); + row.contexts.forEach(context => rowMap[context.guid] = row); + rows.push(row); + }, this); + }, this); + } + + // Find slices attributed to each row. + // TODO(xiaochengh): We should implement a getter + // BlameContextSnapshot.attributedEvents, instead of process the model in + // a UI component. + for (const proc of Object.values(this.model_.processes)) { + for (const thread of Object.values(proc.threads)) { + thread.sliceGroup.iterSlicesInTimeRange(function(topLevelSlice) { + topLevelSlice.contexts.forEach(function(context) { + if (!context.snapshot.guid || !rowMap[context.snapshot.guid]) { + return; + } + const row = rowMap[context.snapshot.guid]; + row.eventsOfInterest.push(topLevelSlice); + row.time += topLevelSlice.selfTime || 0; + }); + }, this.currentRangeOfInterest.min, this.currentRangeOfInterest.max); + } + } + + // Apply grouping to rows. + const select = this.$.select; + const groupOption = select.options[select.selectedIndex].value; + const groupFunction = groupFunctions[groupOption]; + return groupFunction(rows, rowMap); + }, + + updateContents_() { + this.$.table.tableRows = this.createFrameDataTableRows_(); + this.$.table.rebuild(); + }, + + supportsModel(m) { + if (!m) { + return { + supported: false, + reason: 'No model available.' + }; + } + + const ans = {supported: false}; + for (const proc of Object.values(m.processes)) { + proc.objects.iterObjectInstances(function(instance) { + if (instance instanceof BlameContextInstance) { + ans.supported = true; + } + }); + } + + if (!ans.supported) { + ans.reason = 'No frame data available'; + } + return ans; + }, + + get currentRangeOfInterest() { + if (this.rangeOfInterest_.isEmpty) { + return this.model_.bounds; + } + return this.rangeOfInterest_; + }, + + get rangeOfInterest() { + return this.rangeOfInterest_; + }, + + set rangeOfInterest(rangeOfInterest) { + this.rangeOfInterest_ = rangeOfInterest; + this.updateContents_(); + }, + + get selection() { + // Not applicable. + }, + + set selection(_) { + // Not applicable. + }, + + get textLabel() { + return 'Frame Data'; + }, + + get model() { + return this.model_; + }, + + set model(model) { + this.model_ = model; + this.updateContents_(); + } + }); + + tr.ui.side_panel.SidePanelRegistry.register(function() { + return document.createElement('tr-ui-e-s-frame-data-side-panel'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html new file mode 100644 index 00000000000..298afe05d42 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html @@ -0,0 +1,165 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/blame_context/frame_tree_node.html"> +<link rel="import" href="/tracing/extras/chrome/blame_context/render_frame.html"> +<link rel="import" href="/tracing/extras/chrome/blame_context/top_level.html"> +<link rel="import" href="/tracing/ui/extras/side_panel/frame_data_side_panel.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const TestUtils = tr.c.TestUtils; + + function topLevelOptions(pid, id) { + return { + pid, + id, + cat: 'blink', + scope: 'PlatformThread', + name: 'TopLevel' + }; + } + + function renderFrameOptions(pid, id, parent) { + return { + pid, + id, + cat: 'blink', + scope: 'RenderFrame', + name: 'RenderFrame', + args: {parent: { + id_ref: parent.id, + scope: parent.scope + }} + }; + } + + function frameTreeNodeOptions(pid, id, opt_renderFrame, opt_parentId) { + const ans = { + pid, + id, + cat: 'navigation', + scope: 'FrameTreeNode', + name: 'FrameTreeNode', + args: {} + }; + if (opt_renderFrame) { + ans.args.renderFrame = { + id_ref: opt_renderFrame.id, + pid_ref: opt_renderFrame.pid, + scope: 'RenderFrame' + }; + } + if (opt_parentId) { + ans.args.parent = { + id_ref: opt_parentId, + scope: 'FrameTreeNode' + }; + } + return ans; + } + + /** + * Creates some independent contexts. Checks if all are present in the panel. + */ + test('basic', function() { + const panel = document.createElement('tr-ui-e-s-frame-data-side-panel'); + panel.model = TestUtils.newModel(function(model) { + TestUtils.newSnapshot(model, topLevelOptions(1, '0x1')); + TestUtils.newSnapshot(model, renderFrameOptions( + 1, '0x2', {id: '0x1', scope: 'PlatformThread'})); + TestUtils.newSnapshot(model, frameTreeNodeOptions( + 2, '0x3')); + }); + assert.lengthOf(panel.$.table.tableRows, 3); + + this.addHTMLOutput(panel); + }); + + /** + * Creates a FrameTreeNode in the browser process and a RenderFrame in a + * renderer process that are the same frame. Checks if they are merged into + * one row in the panel. + */ + test('mergeCrossProcessFrameBlameContexts', function() { + const panel = document.createElement('tr-ui-e-s-frame-data-side-panel'); + panel.model = TestUtils.newModel(function(model) { + TestUtils.newSnapshot(model, topLevelOptions(1, '0x1')); + TestUtils.newSnapshot(model, renderFrameOptions( + 1, '0x2', {id: '0x1', scope: 'PlatformThread'})); + TestUtils.newSnapshot(model, frameTreeNodeOptions( + 2, '0x3', {id: '0x2', pid: 1})); + }); + assert.lengthOf(panel.$.table.tableRows, 2); + + this.addHTMLOutput(panel); + }); + + function newAttributedSlice(model, pid, start, duration, context) { + const slice = TestUtils.newSliceEx({start, duration}); + slice.contexts = [{type: 'FrameBlameContext', snapshot: context}]; + model.getOrCreateProcess(pid).getOrCreateThread(1).sliceGroup.pushSlice( + slice); + return slice; + } + + /** + * Changes the range of interest. Checks if the panel updates correspondingly. + */ + test('respondToRangeOfInterest', function() { + let topLevel; + let slice1; + let slice2; + const panel = document.createElement('tr-ui-e-s-frame-data-side-panel'); + panel.model = TestUtils.newModel(function(model) { + topLevel = TestUtils.newSnapshot(model, topLevelOptions(1, '0x1')); + slice1 = newAttributedSlice(model, 1, 1500, 500, topLevel); + slice2 = newAttributedSlice(model, 1, 2500, 500, topLevel); + }); + + // The default range of interest contains both slices. + assert.isTrue(panel.$.table.tableRows[0].eventsOfInterest.equals( + new tr.model.EventSet([topLevel, slice1, slice2]))); + + // The new range of interest contains only slice2. + panel.rangeOfInterest = tr.b.math.Range.fromExplicitRange(slice2.start, + slice2.end); + assert.isTrue(panel.$.table.tableRows[0].eventsOfInterest.equals( + new tr.model.EventSet([topLevel, slice2]))); + + this.addHTMLOutput(panel); + }); + + /** + * Selects a row in the panel. Checks if the context(s) of the row and the + * slices attributed to the row are selected. + */ + test('selectAttributedEvents', function() { + let topLevel; + let slice; + const panel = document.createElement('tr-ui-e-s-frame-data-side-panel'); + panel.model = TestUtils.newModel(function(model) { + topLevel = TestUtils.newSnapshot(model, topLevelOptions(1, '0x1')); + slice = newAttributedSlice(model, 1, 1500, 500, topLevel); + }); + + let selectionChanged = false; + panel.addEventListener('requestSelectionChange', function(e) { + selectionChanged = true; + assert.isTrue( + e.selection.equals(new tr.model.EventSet([topLevel, slice]))); + }); + panel.$.table.selectedTableRow = panel.$.table.tableRows[0]; + assert.isTrue(selectionChanged); + + this.addHTMLOutput(panel); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html new file mode 100644 index 00000000000..14e33919922 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html @@ -0,0 +1,334 @@ +<!DOCTYPE html> +<!-- +Copyright 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/line_chart.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html"> + +<dom-module id='tr-ui-e-s-input-latency-side-panel'> + <template> + <style> + :host { + flex-direction: column; + display: flex; + } + toolbar { + flex: 0 0 auto; + border-bottom: 1px solid black; + display: flex; + } + result-area { + flex: 1 1 auto; + display: block; + min-height: 0; + overflow-y: auto; + } + </style> + + <toolbar id='toolbar'></toolbar> + <result-area id='result_area'></result-area> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-s-input-latency-side-panel', + behaviors: [tr.ui.behaviors.SidePanel], + + + ready() { + this.rangeOfInterest_ = new tr.b.math.Range(); + this.frametimeType_ = tr.model.helpers.IMPL_FRAMETIME_TYPE; + this.latencyChart_ = undefined; + this.frametimeChart_ = undefined; + this.selectedProcessId_ = undefined; + this.mouseDownIndex_ = undefined; + this.curMouseIndex_ = undefined; + }, + + get model() { + return this.model_; + }, + + set model(model) { + this.model_ = model; + if (this.model_) { + this.modelHelper_ = this.model_.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + } else { + this.modelHelper_ = undefined; + } + + this.updateToolbar_(); + this.updateContents_(); + }, + + get frametimeType() { + return this.frametimeType_; + }, + + set frametimeType(type) { + if (this.frametimeType_ === type) return; + + this.frametimeType_ = type; + this.updateContents_(); + }, + + get selectedProcessId() { + return this.selectedProcessId_; + }, + + set selectedProcessId(process) { + if (this.selectedProcessId_ === process) return; + + this.selectedProcessId_ = process; + this.updateContents_(); + }, + + set selection(selection) { + if (this.latencyChart_ === undefined) return; + + this.latencyChart_.brushedRange = selection.bounds; + }, + + // This function is for testing purpose. + setBrushedIndices(mouseDownIndex, curIndex) { + this.mouseDownIndex_ = mouseDownIndex; + this.curMouseIndex_ = curIndex; + this.updateBrushedRange_(); + }, + + updateBrushedRange_() { + if (this.latencyChart_ === undefined) return; + + let r = new tr.b.math.Range(); + if (this.mouseDownIndex_ === undefined) { + this.latencyChart_.brushedRange = r; + return; + } + r = this.latencyChart_.computeBrushRangeFromIndices( + this.mouseDownIndex_, this.curMouseIndex_); + this.latencyChart_.brushedRange = r; + + // Based on the brushed range, update the selection of LatencyInfo in + // the timeline view by sending a selectionChange event. + let latencySlices = []; + for (const thread of this.model_.getAllThreads()) { + for (const event of thread.getDescendantEvents()) { + if (event.title.indexOf('InputLatency:') === 0) { + latencySlices.push(event); + } + } + } + latencySlices = tr.model.helpers.getSlicesIntersectingRange( + r, latencySlices); + + const event = new tr.model.RequestSelectionChangeEvent(); + event.selection = new tr.model.EventSet(latencySlices); + this.latencyChart_.dispatchEvent(event); + }, + + registerMouseEventForLatencyChart_() { + this.latencyChart_.addEventListener('item-mousedown', function(e) { + this.mouseDownIndex_ = e.index; + this.curMouseIndex_ = e.index; + this.updateBrushedRange_(); + }.bind(this)); + + this.latencyChart_.addEventListener('item-mousemove', function(e) { + if (e.button === undefined) return; + + this.curMouseIndex_ = e.index; + this.updateBrushedRange_(); + }.bind(this)); + + this.latencyChart_.addEventListener('item-mouseup', function(e) { + this.curMouseIndex = e.index; + this.updateBrushedRange_(); + }.bind(this)); + }, + + updateToolbar_() { + const browserProcess = this.modelHelper_.browserProcess; + const labels = []; + + if (browserProcess !== undefined) { + const labelStr = 'Browser: ' + browserProcess.pid; + labels.push({label: labelStr, value: browserProcess.pid}); + } + + for (const rendererHelper of + Object.values(this.modelHelper_.rendererHelpers)) { + const rendererProcess = rendererHelper.process; + const labelStr = 'Renderer: ' + rendererProcess.userFriendlyName; + labels.push({label: labelStr, value: rendererProcess.userFriendlyName}); + } + + if (labels.length === 0) return; + + this.selectedProcessId_ = labels[0].value; + const toolbarEl = this.$.toolbar; + Polymer.dom(toolbarEl).appendChild(tr.ui.b.createSelector( + this, 'frametimeType', + 'inputLatencySidePanel.frametimeType', this.frametimeType_, + [{label: 'Main Thread Frame Times', + value: tr.model.helpers.MAIN_FRAMETIME_TYPE}, + {label: 'Impl Thread Frame Times', + value: tr.model.helpers.IMPL_FRAMETIME_TYPE} + ])); + Polymer.dom(toolbarEl).appendChild(tr.ui.b.createSelector( + this, 'selectedProcessId', + 'inputLatencySidePanel.selectedProcessId', + this.selectedProcessId_, + labels)); + }, + + // TODO(charliea): Delete this function in favor of rangeOfInterest. + get currentRangeOfInterest() { + if (this.rangeOfInterest_.isEmpty) { + return this.model_.bounds; + } + return this.rangeOfInterest_; + }, + + createLatencyLineChart(data, title, parentNode) { + const chart = new tr.ui.b.LineChart(); + Polymer.dom(parentNode).appendChild(chart); + let width = 600; + if (document.body.clientWidth !== undefined) { + width = document.body.clientWidth * 0.5; + } + chart.graphWidth = width; + chart.chartTitle = title; + chart.data = data; + return chart; + }, + + updateContents_() { + const resultArea = this.$.result_area; + this.latencyChart_ = undefined; + this.frametimeChart_ = undefined; + Polymer.dom(resultArea).textContent = ''; + + if (this.modelHelper_ === undefined) return; + + const rangeOfInterest = this.currentRangeOfInterest; + + let chromeProcess; + if (this.modelHelper_.rendererHelpers[this.selectedProcessId_]) { + chromeProcess = this.modelHelper_.rendererHelpers[ + this.selectedProcessId_ + ]; + } else { + chromeProcess = this.modelHelper_.browserHelper; + } + + const frameEvents = chromeProcess.getFrameEventsInRange( + this.frametimeType, rangeOfInterest); + + const frametimeData = tr.model.helpers.getFrametimeDataFromEvents( + frameEvents); + const averageFrametime = tr.b.math.Statistics.mean(frametimeData, d => + d.frametime + ); + + const latencyEvents = this.modelHelper_.browserHelper. + getLatencyEventsInRange( + rangeOfInterest); + + const latencyData = []; + latencyEvents.forEach(function(event) { + if (event.inputLatency === undefined) return; + + latencyData.push({ + x: event.start, + latency: event.inputLatency / 1000 + }); + }); + + const averageLatency = tr.b.math.Statistics.mean(latencyData, function(d) { + return d.latency; + }); + + // Create summary. + const latencySummaryText = document.createElement('div'); + Polymer.dom(latencySummaryText).appendChild(tr.ui.b.createSpan({ + textContent: 'Average Latency ' + averageLatency + ' ms', + bold: true})); + Polymer.dom(resultArea).appendChild(latencySummaryText); + + const frametimeSummaryText = document.createElement('div'); + Polymer.dom(frametimeSummaryText).appendChild(tr.ui.b.createSpan({ + textContent: 'Average Frame Time ' + averageFrametime + ' ms', + bold: true})); + Polymer.dom(resultArea).appendChild(frametimeSummaryText); + + if (latencyData.length !== 0) { + this.latencyChart_ = this.createLatencyLineChart( + latencyData, 'Latency Over Time', resultArea); + this.registerMouseEventForLatencyChart_(); + } + + if (frametimeData.length !== 0) { + this.frametimeChart_ = this.createLatencyLineChart( + frametimeData, 'Frame Times', resultArea); + } + }, + + get rangeOfInterest() { + return this.rangeOfInterest_; + }, + + set rangeOfInterest(rangeOfInterest) { + this.rangeOfInterest_ = rangeOfInterest; + this.updateContents_(); + }, + + supportsModel(m) { + if (m === undefined) { + return { + supported: false, + reason: 'Unknown tracing model' + }; + } + + if (!tr.model.helpers.ChromeModelHelper.supportsModel(m)) { + return { + supported: false, + reason: 'No Chrome browser or renderer process found' + }; + } + + const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + if (modelHelper.browserHelper && + modelHelper.browserHelper.hasLatencyEvents) { + return { + supported: true + }; + } + + return { + supported: false, + reason: 'No InputLatency events trace. Consider enabling ' + + 'benchmark" and "input" category when recording the trace' + }; + }, + + get textLabel() { + return 'Input Latency'; + } +}); + +tr.ui.side_panel.SidePanelRegistry.register(function() { + return document.createElement('tr-ui-e-s-input-latency-side-panel'); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html new file mode 100644 index 00000000000..de225416faa --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html @@ -0,0 +1,148 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/side_panel/input_latency_side_panel.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('basic', function() { + const latencyData = [ + { + x: 1000, + latency: 16 + }, + { + x: 2000, + latency: 17 + }, + { + x: 3000, + latency: 14 + }, + { + x: 4000, + latency: 23 + } + ]; + let lc = document.createElement('tr-ui-e-s-input-latency-side-panel'); + let container = document.createElement('div'); + this.addHTMLOutput(container); + const latencyChart = lc.createLatencyLineChart( + latencyData, 'latency', container); + + const frametimeData = [ + { + x: 1000, + frametime: 16 + }, + { + x: 2000, + frametime: 17 + }, + { + x: 3000, + frametime: 14 + }, + { + x: 4000, + frametime: 23 + } + ]; + lc = document.createElement('tr-ui-e-s-input-latency-side-panel'); + container = document.createElement('div'); + this.addHTMLOutput(container); + const frametimeChart = lc.createLatencyLineChart( + frametimeData, 'frametime', container); + }); + + test('brushedRangeChange', function() { + const events = []; + for (let i = 0; i < 10; i++) { + const startTs = i * 10000; + const endTs = startTs + 1000 * (i % 2); + events.push( + { + 'cat': 'benchmark', + 'pid': 3507, + 'tid': 3507, + 'ts': startTs, + 'ph': 'S', + 'name': 'InputLatency', + 'id': i + }); + events.push( + { + 'cat': 'benchmark', + 'pid': 3507, + 'tid': 3507, + 'ts': endTs, + 'ph': 'T', + 'name': 'InputLatency', + 'args': {'step': 'GestureScrollUpdate'}, + 'id': i + }); + events.push( + { + 'cat': 'benchmark', + 'pid': 3507, + 'tid': 3507, + 'ts': endTs, + 'ph': 'F', + 'name': 'InputLatency', + 'args': { + 'data': { + 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT': { + 'time': startTs + }, + 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT': { + 'time': endTs + } + } + }, + 'id': i + }); + } + events.push({'cat': '__metadata', + 'pid': 3507, + 'tid': 3507, + 'ts': 0, + 'ph': 'M', + 'name': 'thread_name', + 'args': {'name': 'CrBrowserMain'}}); + + const panel = document.createElement('tr-ui-e-s-input-latency-side-panel'); + this.addHTMLOutput(panel); + + let selectionChanged = false; + + panel.model = tr.c.TestUtils.newModelWithEvents([events]); + function listener(e) { + selectionChanged = true; + assert.strictEqual(e.selection.length, 3); + const predictedStarts = [20, 31, 40]; + let i = 0; + for (const event of e.selection) { + assert.strictEqual(event.start, predictedStarts[i++]); + } + } + panel.ownerDocument.addEventListener('requestSelectionChange', listener); + try { + panel.setBrushedIndices(2, 4); + } finally { + panel.ownerDocument.removeEventListener( + 'requestSelectionChange', listener); + } + assert.isTrue(selectionChanged); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html new file mode 100644 index 00000000000..31bc1dbd997 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/system_stats/system_stats_snapshot.html"> +<link rel="import" + href="/tracing/ui/extras/system_stats/system_stats_instance_track.html"> +<link rel="import" + href="/tracing/ui/extras/system_stats/system_stats_snapshot_view.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css new file mode 100644 index 00000000000..40096f5497c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css @@ -0,0 +1,15 @@ +/* Copyright (c) 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +.tr-ui-e-system-stats-instance-track { + height: 500px; +} + +.tr-ui-e-system-stats-instance-track ul { + list-style: none; + list-style-position: outside; + margin: 0; + overflow: hidden; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html new file mode 100644 index 00000000000..7695086660c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html @@ -0,0 +1,451 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="stylesheet" + href="/tracing/ui/extras/system_stats/system_stats_instance_track.css"> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/event_presenter.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/object_instance_track.html"> +<link rel="import" href="/tracing/ui/tracks/stacked_bars_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.system_stats', function() { + const EventPresenter = tr.ui.b.EventPresenter; + + let statCount; + + const excludedStats = {'meminfo': { + 'pswpin': 0, + 'pswpout': 0, + 'pgmajfault': 0}, + 'diskinfo': { + 'io': 0, + 'io_time': 0, + 'read_time': 0, + 'reads': 0, + 'reads_merged': 0, + 'sectors_read': 0, + 'sectors_written': 0, + 'weighted_io_time': 0, + 'write_time': 0, + 'writes': 0, + 'writes_merged': 0}, + 'swapinfo': {}, + 'perfinfo': { + 'idle_time': 0, + 'read_transfer_count': 0, + 'write_transfer_count': 0, + 'other_transfer_count': 0, + 'read_operation_count': 0, + 'write_operation_count': 0, + 'other_operation_count': 0, + 'pagefile_pages_written': 0, + 'pagefile_pages_write_ios': 0, + 'available_pages': 0, + 'pages_read': 0, + 'page_read_ios': 0} + }; + + /** + * Tracks that display system stats data. + * + * @constructor + * @extends {StackedBarsTrack} + */ + + const SystemStatsInstanceTrack = tr.ui.b.define( + 'tr-ui-e-system-stats-instance-track', tr.ui.tracks.StackedBarsTrack); + + const kPageSizeWindows = 4096; + + SystemStatsInstanceTrack.prototype = { + + __proto__: tr.ui.tracks.StackedBarsTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.StackedBarsTrack.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('tr-ui-e-system-stats-instance-track'); + this.objectInstance_ = null; + }, + + set objectInstances(objectInstances) { + if (!objectInstances) { + this.objectInstance_ = []; + return; + } + if (objectInstances.length !== 1) { + throw new Error('Bad object instance count.'); + } + this.objectInstance_ = objectInstances[0]; + if (this.objectInstance_ !== null) { + this.computeRates_(this.objectInstance_.snapshots); + this.maxStats_ = this.computeMaxStats_( + this.objectInstance_.snapshots); + } + }, + + computeRates_(snapshots) { + for (let i = 0; i < snapshots.length; i++) { + const snapshot = snapshots[i]; + const stats = snapshot.getStats(); + let prevSnapshot; + + if (i === 0) { + // Deltas will be zero. + prevSnapshot = snapshots[0]; + } else { + prevSnapshot = snapshots[i - 1]; + } + const prevStats = prevSnapshot.getStats(); + let timeIntervalSeconds = (snapshot.ts - prevSnapshot.ts) / 1000; + // Prevent divide by zero. + if (timeIntervalSeconds === 0) { + timeIntervalSeconds = 1; + } + + this.computeRatesRecursive_(prevStats, stats, + timeIntervalSeconds); + } + }, + + computeRatesRecursive_(prevStats, stats, + timeIntervalSeconds) { + for (const statName in stats) { + if (stats[statName] instanceof Object) { + this.computeRatesRecursive_(prevStats[statName], + stats[statName], + timeIntervalSeconds); + } else { + if (statName === 'sectors_read') { + stats.bytes_read_per_sec = (stats.sectors_read - + prevStats.sectors_read) * + 512 / timeIntervalSeconds; + } + if (statName === 'sectors_written') { + stats.bytes_written_per_sec = + (stats.sectors_written - + prevStats.sectors_written) * + 512 / timeIntervalSeconds; + } + if (statName === 'pgmajfault') { + stats.pgmajfault_per_sec = (stats.pgmajfault - + prevStats.pgmajfault) / + timeIntervalSeconds; + } + if (statName === 'pswpin') { + stats.bytes_swpin_per_sec = (stats.pswpin - + prevStats.pswpin) * + 1000 / timeIntervalSeconds; + } + if (statName === 'pswpout') { + stats.bytes_swpout_per_sec = (stats.pswpout - + prevStats.pswpout) * + 1000 / timeIntervalSeconds; + } + + // All the stats below are available only on Windows: + + if (statName === 'idle_time') { + // Total amount of idle_time, in unit of 100 nanoseconds. + const units = tr.b.convertUnit(100., + tr.b.UnitScale.TIME.NANO_SEC, tr.b.UnitScale.TIME.SEC); + const idleTile = (stats.idle_time - prevStats.idle_time) * units; + stats.idle_time_per_sec = idleTile / timeIntervalSeconds; + } + if (statName === 'read_transfer_count') { + const bytesRead = stats.read_transfer_count - + prevStats.read_transfer_count; + stats.bytes_read_per_sec = bytesRead / timeIntervalSeconds; + } + if (statName === 'write_transfer_count') { + const bytesWritten = stats.write_transfer_count - + prevStats.write_transfer_count; + stats.bytes_written_per_sec = bytesWritten / timeIntervalSeconds; + } + if (statName === 'other_transfer_count') { + const bytesTransfer = stats.other_transfer_count - + prevStats.other_transfer_count; + stats.bytes_other_per_sec = bytesTransfer / timeIntervalSeconds; + } + if (statName === 'read_operation_count') { + const readOperation = stats.read_operation_count - + prevStats.read_operation_count; + stats.read_operation_per_sec = readOperation / timeIntervalSeconds; + } + if (statName === 'write_operation_count') { + const writeOperation = stats.write_operation_count - + prevStats.write_operation_count; + stats.write_operation_per_sec = + writeOperation / timeIntervalSeconds; + } + if (statName === 'other_operation_count') { + const otherOperation = stats.other_operation_count - + prevStats.other_operation_count; + stats.other_operation_per_sec = + otherOperation / timeIntervalSeconds; + } + if (statName === 'pagefile_pages_written') { + const pageFileBytesWritten = + (stats.pagefile_pages_written - + prevStats.pagefile_pages_written) * kPageSizeWindows; + stats.pagefile_bytes_written_per_sec = + pageFileBytesWritten / timeIntervalSeconds; + } + if (statName === 'pagefile_pages_write_ios') { + const pagefileWriteOperation = + stats.pagefile_pages_write_ios - + prevStats.pagefile_pages_write_ios; + stats.pagefile_write_operation_per_sec = + pagefileWriteOperation / timeIntervalSeconds; + } + if (statName === 'available_pages') { + // Nothing to do here for now. + stats.available_pages_in_bytes = + stats.available_pages * kPageSizeWindows; + // TODO(sebmarchand): Add a available_pages_field that tracks the + // variation of this metric? + } + if (statName === 'pages_read') { + const pagesBytesRead = + (stats.pages_read - prevStats.pages_read) * kPageSizeWindows; + stats.bytes_read_per_sec = pagesBytesRead / timeIntervalSeconds; + } + if (statName === 'page_read_ios') { + const pagesBytesReadOperations = + stats.page_read_ios - prevStats.page_read_ios; + stats.pagefile_write_operation_per_sec = + pagesBytesReadOperations / timeIntervalSeconds; + } + } + } + }, + + computeMaxStats_(snapshots) { + const maxStats = {}; + statCount = 0; + + for (let i = 0; i < snapshots.length; i++) { + const snapshot = snapshots[i]; + const stats = snapshot.getStats(); + + this.computeMaxStatsRecursive_(stats, maxStats, + excludedStats); + } + + return maxStats; + }, + + computeMaxStatsRecursive_(stats, maxStats, excludedStats) { + for (const statName in stats) { + if (stats[statName] instanceof Object) { + if (!(statName in maxStats)) { + maxStats[statName] = {}; + } + + let excludedNested; + if (excludedStats && statName in excludedStats) { + excludedNested = excludedStats[statName]; + } else { + excludedNested = null; + } + + this.computeMaxStatsRecursive_(stats[statName], + maxStats[statName], + excludedNested); + } else { + if (excludedStats && statName in excludedStats) { + continue; + } + if (!(statName in maxStats)) { + maxStats[statName] = 0; + statCount++; + } + if (stats[statName] > maxStats[statName]) { + maxStats[statName] = stats[statName]; + } + } + } + }, + + get height() { + return window.getComputedStyle(this).height; + }, + + set height(height) { + this.style.height = height; + }, + + draw(type, viewLWorld, viewRWorld, viewHeight) { + switch (type) { + case tr.ui.tracks.DrawType.GENERAL_EVENT: + this.drawStatBars_(viewLWorld, viewRWorld); + break; + } + }, + + drawStatBars_(viewLWorld, viewRWorld) { + const ctx = this.context(); + const pixelRatio = window.devicePixelRatio || 1; + + const bounds = this.getBoundingClientRect(); + const width = bounds.width * pixelRatio; + const height = (bounds.height * pixelRatio) / statCount; + + // Culling parameters. + const vp = this.viewport.currentDisplayTransform; + + // Scale by the size of the largest snapshot. + const maxStats = this.maxStats_; + + const objectSnapshots = this.objectInstance_.snapshots; + let lowIndex = tr.b.findLowIndexInSortedArray( + objectSnapshots, + function(snapshot) { + return snapshot.ts; + }, + viewLWorld); + + // Assure that the stack with the left edge off screen still gets drawn + if (lowIndex > 0) lowIndex -= 1; + + for (let i = lowIndex; i < objectSnapshots.length; ++i) { + const snapshot = objectSnapshots[i]; + const trace = snapshot.getStats(); + const currentY = height; + + const left = snapshot.ts; + if (left > viewRWorld) break; + + let leftView = vp.xWorldToView(left); + if (leftView < 0) leftView = 0; + + // Compute the edges for the column graph bar. + let right; + if (i !== objectSnapshots.length - 1) { + right = objectSnapshots[i + 1].ts; + } else { + // If this is the last snapshot of multiple snapshots, use the width + // of the previous snapshot for the width. + if (objectSnapshots.length > 1) { + right = objectSnapshots[i].ts + (objectSnapshots[i].ts - + objectSnapshots[i - 1].ts); + } else { + // If there's only one snapshot, use max bounds as the width. + right = this.objectInstance_.parent.model.bounds.max; + } + } + + let rightView = vp.xWorldToView(right); + if (rightView > width) { + rightView = width; + } + + // Floor the bounds to avoid a small gap between stacks. + leftView = Math.floor(leftView); + rightView = Math.floor(rightView); + + // Descend into nested stats. + this.drawStatBarsRecursive_(snapshot, + leftView, + rightView, + height, + trace, + maxStats, + currentY); + + if (i === lowIndex) { + this.drawStatNames_(leftView, height, currentY, '', maxStats); + } + } + ctx.lineWidth = 1; + }, + + drawStatBarsRecursive_(snapshot, + leftView, + rightView, + height, + stats, + maxStats, + currentY) { + const ctx = this.context(); + + for (const statName in maxStats) { + if (stats[statName] instanceof Object) { + // Use the y-position returned from the recursive call. + currentY = this.drawStatBarsRecursive_(snapshot, + leftView, + rightView, + height, + stats[statName], + maxStats[statName], + currentY); + } else { + const maxStat = maxStats[statName]; + + // Draw a bar for the stat. The height of the bar is scaled + // against the largest value of the stat across all snapshots. + ctx.fillStyle = EventPresenter.getBarSnapshotColor( + snapshot, Math.round(currentY / height)); + + let barHeight; + if (maxStat > 0) { + barHeight = height * Math.max(stats[statName], 0) / maxStat; + } else { + barHeight = 0; + } + + ctx.fillRect(leftView, currentY - barHeight, + Math.max(rightView - leftView, 1), barHeight); + + currentY += height; + } + } + + // Return the updated y-position. + return currentY; + }, + + drawStatNames_(leftView, height, currentY, prefix, maxStats) { + const ctx = this.context(); + + ctx.textAlign = 'end'; + ctx.font = '12px Arial'; + ctx.fillStyle = '#000000'; + for (const statName in maxStats) { + if (maxStats[statName] instanceof Object) { + currentY = this.drawStatNames_(leftView, height, currentY, + statName, maxStats[statName]); + } else { + let fullname = statName; + + if (prefix !== '') { + fullname = prefix + ' :: ' + statName; + } + + ctx.fillText(fullname, leftView - 10, currentY - height / 4); + currentY += height; + } + } + + return currentY; + } + }; + + tr.ui.tracks.ObjectInstanceTrack.register( + SystemStatsInstanceTrack, + {typeName: 'base::TraceEventSystemStatsMonitor::SystemStats'}); + + return { + SystemStatsInstanceTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html new file mode 100644 index 00000000000..8dc4bc28264 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html @@ -0,0 +1,116 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/extras/system_stats/system_stats.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const SystemStatsInstanceTrack = + tr.ui.e.system_stats.SystemStatsInstanceTrack; + const Viewport = tr.ui.TimelineViewport; + + const createObjects = function() { + const objectInstance = new tr.model.ObjectInstance({}); + const snapshots = []; + + const stats1 = {}; + const stats2 = {}; + + stats1.committed_memory = 2000000; + stats2.committed_memory = 3000000; + + stats1.meminfo = {}; + stats1.meminfo.free = 10000; + stats2.meminfo = {}; + stats2.meminfo.free = 20000; + + stats1.perfinfo = {}; + stats1.perfinfo.idle_time = 10; + stats1.perfinfo.read_transfer_count = 20; + stats1.perfinfo.write_transfer_count = 30; + stats1.perfinfo.other_transfer_count = 40; + stats1.perfinfo.read_operation_count = 2; + stats1.perfinfo.write_operation_count = 3; + stats1.perfinfo.other_operation_count = 4; + stats1.perfinfo.pagefile_pages_written = 5; + stats1.perfinfo.pagefile_pages_write_ios = 6; + + stats2.perfinfo = {}; + stats2.perfinfo.idle_time = 110; + stats2.perfinfo.read_transfer_count = 120; + stats2.perfinfo.write_transfer_count = 130; + stats2.perfinfo.other_transfer_count = 140; + stats2.perfinfo.read_operation_count = 102; + stats2.perfinfo.write_operation_count = 103; + stats2.perfinfo.other_operation_count = 104; + stats2.perfinfo.pagefile_pages_written = 105; + stats2.perfinfo.pagefile_pages_write_ios = 106; + + snapshots.push(new tr.e.system_stats.SystemStatsSnapshot(objectInstance, + 10, stats1)); + snapshots.push(new tr.e.system_stats.SystemStatsSnapshot(objectInstance, + 20, stats2)); + + objectInstance.snapshots = snapshots; + + return objectInstance; + }; + + test('instantiate', function() { + const objectInstances = []; + objectInstances.push(createObjects()); + + const div = document.createElement('div'); + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = new SystemStatsInstanceTrack(viewport); + track.objectInstances = objectInstances; + Polymer.dom(drawingContainer).appendChild(track); + + const snapshot1 = track.objectInstance_.snapshots[1]; + const stats1 = snapshot1.getStats(); + + // Raw counters should not move. + assert.strictEqual(stats1.perfinfo.idle_time, 110); + assert.strictEqual(stats1.perfinfo.read_operation_count, 102); + assert.strictEqual(stats1.perfinfo.write_operation_count, 103); + assert.strictEqual(stats1.perfinfo.other_operation_count, 104); + assert.strictEqual(stats1.perfinfo.read_transfer_count, 120); + assert.strictEqual(stats1.perfinfo.write_transfer_count, 130); + assert.strictEqual(stats1.perfinfo.other_transfer_count, 140); + assert.strictEqual(stats1.perfinfo.pagefile_pages_written, 105); + assert.strictEqual(stats1.perfinfo.pagefile_pages_write_ios, 106); + + // Rates should be computed. + assert.strictEqual(stats1.perfinfo.idle_time_per_sec, 0.001); + assert.strictEqual(stats1.perfinfo.bytes_read_per_sec, 10000); + assert.strictEqual(stats1.perfinfo.bytes_written_per_sec, 10000); + assert.strictEqual(stats1.perfinfo.bytes_other_per_sec, 10000); + assert.strictEqual(stats1.perfinfo.read_operation_per_sec, 10000); + assert.strictEqual(stats1.perfinfo.write_operation_per_sec, 10000); + assert.strictEqual(stats1.perfinfo.other_operation_per_sec, 10000); + assert.strictEqual(stats1.perfinfo.pagefile_bytes_written_per_sec, + 40960000); + assert.strictEqual(stats1.perfinfo.pagefile_write_operation_per_sec, 10000); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + track.heading = 'testBasic'; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 50, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css new file mode 100644 index 00000000000..e698b15aa70 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css @@ -0,0 +1,28 @@ +/* Copyright (c) 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +.tr-ui-e-system-stats-snapshot-view .subhead { + font-size: small; + padding-bottom: 10px; +} + +.tr-ui-e-system-stats-snapshot-view ul { + background-position: 0 5px; + background-repeat: no-repeat; + cursor: pointer; + font-family: monospace; + list-style: none; + margin: 0; + padding-left: 15px; +} + +.tr-ui-e-system-stats-snapshot-view li { + background-position: 0 5px; + background-repeat: no-repeat; + cursor: pointer; + list-style: none; + margin: 0; + padding-left: 15px; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html new file mode 100644 index 00000000000..54f4b869f4a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="stylesheet" + href="/tracing/ui/extras/system_stats/system_stats_snapshot_view.css"> + +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.system_stats', function() { + /* + * Displays a system stats snapshot in a human readable form. @constructor + */ + const SystemStatsSnapshotView = tr.ui.b.define( + 'tr-ui-e-system-stats-snapshot-view', tr.ui.analysis.ObjectSnapshotView); + + SystemStatsSnapshotView.prototype = { + __proto__: tr.ui.analysis.ObjectSnapshotView.prototype, + + decorate() { + Polymer.dom(this).classList.add('tr-ui-e-system-stats-snapshot-view'); + }, + + updateContents() { + const snapshot = this.objectSnapshot_; + if (!snapshot || !snapshot.getStats()) { + Polymer.dom(this).textContent = 'No system stats snapshot found.'; + return; + } + // Clear old snapshot view. + Polymer.dom(this).textContent = ''; + + const stats = snapshot.getStats(); + Polymer.dom(this).appendChild(this.buildList_(stats)); + }, + + isFloat(n) { + return typeof n === 'number' && n % 1 !== 0; + }, + + /** + * Creates nested lists. + * + * @param {Object} stats The current trace system stats entry. + * @return {Element} A ul list element. + */ + buildList_(stats) { + const statList = document.createElement('ul'); + + for (const statName in stats) { + const statText = document.createElement('li'); + Polymer.dom(statText).textContent = '' + statName + ': '; + Polymer.dom(statList).appendChild(statText); + + if (stats[statName] instanceof Object) { + Polymer.dom(statList).appendChild(this.buildList_(stats[statName])); + } else { + if (this.isFloat(stats[statName])) { + Polymer.dom(statText).textContent += stats[statName].toFixed(2); + } else { + Polymer.dom(statText).textContent += stats[statName]; + } + } + } + + return statList; + } + }; + + tr.ui.analysis.ObjectSnapshotView.register( + SystemStatsSnapshotView, + {typeName: 'base::TraceEventSystemStatsMonitor::SystemStats'}); + + return { + SystemStatsSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html new file mode 100644 index 00000000000..fcc410b754a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<!-- +TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the +same way that all non-UI files depend on tracing/base/base.html. Enforce this +dependency with a presubmit. +--> +<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order> + +<link rel="import" href="/tracing/extras/systrace_config.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/extras/side_panel/alerts_side_panel.html"> +<link rel="import" href="/tracing/ui/timeline_view.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html new file mode 100644 index 00000000000..bc3247d9289 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html @@ -0,0 +1,728 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/v8/v8_gc_stats_thread_slice.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-e-v8-gc-objects-stats-table'> + <template> + <style> + tr-ui-b-table { + flex: 0 0 auto; + align-self: stretch; + margin-top: 1em; + font-size: 12px; + } + .diff { + display: inline-block; + margin-top: 1em; + margin-left: 0.8em; + } + </style> + <div class="diff" id="diffOption"> + Diff + </div> + <tr-ui-b-table id="diffTable"></tr-ui-b-table> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.e.v8', function() { + // Instance types that should not be part of the overview as they are either + // double-attributed (e.g. also part of some other instance type) or do not + // make any sense in memory profiling. + const IGNORED_ENTRIES = { + // Ignore code aging entries as they are already accounted in their + // respective code instance types. + match: full => full.startsWith('*CODE_AGE_') + }; + + // Groups are matched on a first-matched basis, i.e., once a group matches we + // are done with an entry. + // Requires properties: + // - match(full): Return true iff |full| should be part of the group and + // false otherwise. + // - keyToName(key): Returns the human readable name for |key|. + // - nameToKey(name): Returns the key for |name|. + // Optional properties: + // - realEntry: A string representing the actual entry in the trace. If this + // entry is present an additional entry UNKNOWN will be created holding all + // the unaccounted data. + const INSTANCE_TYPE_GROUPS = { + FIXED_ARRAY_TYPE: { + match: full => full.startsWith('*FIXED_ARRAY_'), + realEntry: 'FIXED_ARRAY_TYPE', + keyToName: key => key.slice('*FIXED_ARRAY_'.length) + .slice(0, -('_SUB_TYPE'.length)), + nameToKey: name => '*FIXED_ARRAY_' + name + '_SUB_TYPE' + }, + CODE_TYPE: { + match: full => full.startsWith('*CODE_'), + realEntry: 'CODE_TYPE', + keyToName: key => key.slice('*CODE_'.length), + nameToKey: name => '*CODE_' + name + }, + JS_OBJECTS: { + match: full => full.startsWith('JS_'), + keyToName: key => key, + nameToKey: name => name + }, + Strings: { + match: full => full.endsWith('STRING_TYPE'), + keyToName: key => key, + nameToKey: name => name + } + }; + + const DIFF_COLOR = { + GREEN: '#64DD17', + RED: '#D50000' + }; + + function computePercentage(valueA, valueB) { + if (valueA === 0) return 0; + return valueA / valueB * 100; + } + + class DiffEntry { + constructor(originalEntry, diffEntry) { + this.originalEntry_ = originalEntry; + this.diffEntry_ = diffEntry; + } + get title() { + return this.diffEntry_.title; + } + get overall() { + return this.diffEntry_.overall; + } + get overAllocated() { + return this.diffEntry_.overAllocated; + } + get count() { + return this.diffEntry_.count; + } + get overallPercent() { + return this.diffEntry_.overallPercent; + } + get overAllocatedPercent() { + return this.diffEntry_.overAllocatedPercent; + } + get origin() { + return this.originalEntry_; + } + get diff() { + return this.diffEntry_; + } + get subRows() { + return this.diffEntry_.subRows; + } + } + + class Entry { + constructor(title, count, overall, overAllocated, histogram, + overAllocatedHistogram) { + this.title_ = title; + this.overall_ = overall; + this.count_ = count; + this.overAllocated_ = overAllocated; + this.histogram_ = histogram; + this.overAllocatedHistogram_ = overAllocatedHistogram; + this.bucketSize_ = this.histogram_.length; + this.overallPercent_ = 100; + this.overAllocatedPercent_ = 100; + } + + get title() { + return this.title_; + } + + get overall() { + return this.overall_; + } + + get count() { + return this.count_; + } + + get overAllocated() { + return this.overAllocated_; + } + + get histogram() { + return this.histogram_; + } + + get overAllocatedHistogram() { + return this.overAllocatedHistogram_; + } + + get bucketSize() { + return this.bucketSize_; + } + + get overallPercent() { + return this.overallPercent_; + } + + set overallPercent(value) { + this.overallPercent_ = value; + } + + get overAllocatedPercent() { + return this.overAllocatedPercent_; + } + + set overAllocatedPercent(value) { + this.overAllocatedPercent_ = value; + } + + setFromObject(obj) { + this.count_ = obj.count; + // Calculate memory in KB. + this.overall_ = obj.overall / 1024; + this.overAllocated_ = obj.over_allocated / 1024; + this.histogram_ = obj.histogram; + this.overAllocatedHistogram_ = obj.over_allocated_histogram; + } + + diff(other) { + const entry = new Entry(this.title_, other.count_ - this.count, + other.overall_ - this.overall, + other.overAllocated_ - this.overAllocated, [], []); + entry.overallPercent = computePercentage(entry.overall, this.overall); + entry.overAllocatedPercent = computePercentage(entry.overAllocated, + this.overAllocated); + return new DiffEntry(this, entry); + } + } + + class GroupedEntry extends Entry { + constructor(title, count, overall, overAllocated, histogram, + overAllocatedHistogram) { + super(title, count, overall, overAllocated, histogram, + overAllocatedHistogram); + this.histogram_.fill(0); + this.overAllocatedHistogram_.fill(0); + this.entries_ = new Map(); + } + + get title() { + return this.title_; + } + + set title(value) { + this.title_ = value; + } + + get subRows() { + return Array.from(this.entries_.values()); + } + + getEntryFromTitle(title) { + return this.entries_.get(title); + } + + add(entry) { + this.count_ += entry.count; + this.overall_ += entry.overall; + this.overAllocated_ += entry.overAllocated; + if (this.bucketSize_ === entry.bucketSize) { + for (let i = 0; i < this.bucketSize_; ++i) { + this.histogram_[i] += entry.histogram[i]; + this.overAllocatedHistogram_[i] += entry.overAllocatedHistogram[i]; + } + } + this.entries_.set(entry.title, entry); + } + + accumulateUnknown(title) { + let unknownCount = this.count_; + let unknownOverall = this.overall_; + let unknownOverAllocated = this.overAllocated_; + const unknownHistogram = tr.b.deepCopy(this.histogram_); + const unknownOverAllocatedHistogram = + tr.b.deepCopy(this.overAllocatedHistogram_); + for (const entry of this.entries_.values()) { + unknownCount -= entry.count; + unknownOverall -= entry.overall; + unknownOverAllocated -= entry.overAllocated; + for (let i = 0; i < this.bucketSize_; ++i) { + unknownHistogram[i] -= entry.histogram[i]; + unknownOverAllocatedHistogram[i] -= entry.overAllocatedHistogram[i]; + } + } + unknownOverAllocated = + unknownOverAllocated < 0 ? 0 : unknownOverAllocated; + this.entries_.set(title, new Entry(title, unknownCount, unknownOverall, + unknownOverAllocated, unknownHistogram, + unknownOverAllocatedHistogram)); + } + + calculatePercentage() { + for (const entry of this.entries_.values()) { + entry.overallPercent = computePercentage(entry.overall, this.overall_); + entry.overAllocatedPercent = + computePercentage(entry.overAllocated, this.overAllocated_); + + if (entry instanceof GroupedEntry) entry.calculatePercentage(); + } + } + + diff(other) { + let newTitle = ''; + if (this.title_.startsWith('Isolate')) { + newTitle = 'Total'; + } else { + newTitle = this.title_; + } + const result = new GroupedEntry(newTitle, 0, 0, 0, [], []); + for (const entry of this.entries_) { + const otherEntry = other.getEntryFromTitle(entry[0]); + if (otherEntry === undefined) continue; + result.add(entry[1].diff(otherEntry)); + } + result.overallPercent = computePercentage(result.overall, this.overall); + result.overAllocatedPercent = computePercentage(result.overAllocated, + this.overAllocated); + return new DiffEntry(this, result); + } + } + + function createSelector(targetEl, defaultValue, items, callback) { + const selectorEl = document.createElement('select'); + selectorEl.addEventListener('change', callback.bind(targetEl)); + const defaultOptionEl = document.createElement('option'); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const optionEl = document.createElement('option'); + Polymer.dom(optionEl).textContent = item.label; + optionEl.targetPropertyValue = item.value; + optionEl.item = item; + Polymer.dom(selectorEl).appendChild(optionEl); + } + selectorEl.__defineGetter__('selectedValue', function(v) { + if (selectorEl.children[selectorEl.selectedIndex] === undefined) { + return undefined; + } + return selectorEl.children[selectorEl.selectedIndex].targetPropertyValue; + }); + selectorEl.__defineGetter__('selectedItem', function(v) { + if (selectorEl.children[selectorEl.selectedIndex] === undefined) { + return undefined; + } + return selectorEl.children[selectorEl.selectedIndex].item; + }); + selectorEl.__defineSetter__('selectedValue', function(v) { + for (let i = 0; i < selectorEl.children.length; i++) { + const value = selectorEl.children[i].targetPropertyValue; + if (value === v) { + const changed = selectorEl.selectedIndex !== i; + if (changed) { + selectorEl.selectedIndex = i; + callback(); + } + return; + } + } + throw new Error('Not a valid value'); + }); + selectorEl.selectedIndex = -1; + + return selectorEl; + } + + function plusMinus(value, toFixed = 3) { + return (value > 0 ? '+' : '') + value.toFixed(toFixed); + } + + function addArrow(value) { + if (value === 0) return value; + if (value === Number.NEGATIVE_INFINITY) return '\u2193\u221E'; + if (value === Number.POSITIVE_INFINITY) return '\u2191\u221E'; + return (value > 0 ? '\u2191' : '\u2193') + Math.abs(value.toFixed(3)); + } + + Polymer({ + is: 'tr-ui-e-v8-gc-objects-stats-table', + + ready() { + this.$.diffOption.style.display = 'none'; + this.isolateEntries_ = []; + this.selector1_ = undefined; + this.selector2_ = undefined; + }, + + constructDiffTable_(table) { + this.$.diffTable.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + this.$.diffTable.tableColumns = [ + { + title: 'Component', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.title; + return typeEl; + }, + showExpandButtons: true + }, + { + title: 'Overall Memory(KB)', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = row.origin.overall.toFixed(3); + return spanEl; + }, + cmp(a, b) { + return a.origin.overall - b.origin.overall; + } + }, + { + title: 'diff(KB)', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = plusMinus(row.overall); + if (row.overall > 0) { + spanEl.style.color = DIFF_COLOR.RED; + } else if (row.overall < 0) { + spanEl.style.color = DIFF_COLOR.GREEN; + } + return spanEl; + }, + cmp(a, b) { + return a.overall - b.overall; + } + }, + { + title: 'diff(%)', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = addArrow(row.overallPercent); + if (row.overall > 0) { + spanEl.style.color = DIFF_COLOR.RED; + } else if (row.overall < 0) { + spanEl.style.color = DIFF_COLOR.GREEN; + } + return spanEl; + }, + cmp(a, b) { + return a.overall - b.overall; + } + }, + { + title: 'Over Allocated Memory(KB)', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = row.origin.overAllocated.toFixed(3); + return spanEl; + }, + cmp(a, b) { + return a.origin.overAllocated - b.origin.overAllocated; + } + }, + { + title: 'diff(KB)', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = plusMinus(row.overAllocated); + if (row.overAllocated > 0) { + spanEl.style.color = DIFF_COLOR.RED; + } else if (row.overAllocated < 0) { + spanEl.style.color = DIFF_COLOR.GREEN; + } + return spanEl; + }, + cmp(a, b) { + return a.overAllocated - b.overAllocated; + } + }, + { + title: 'diff(%)', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = addArrow(row.overAllocatedPercent); + if (row.overAllocated > 0) { + spanEl.style.color = DIFF_COLOR.RED; + } else if (row.overAllocated < 0) { + spanEl.style.color = DIFF_COLOR.GREEN; + } + return spanEl; + }, + cmp(a, b) { + return a.overAllocated - b.overAllocated; + } + }, + { + title: 'Count', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = row.origin.count; + return spanEl; + }, + cmp(a, b) { + return a.origin.count - b.origin.count; + } + }, + { + title: 'diff', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = plusMinus(row.count, 0); + if (row.count > 0) { + spanEl.style.color = DIFF_COLOR.RED; + } else if (row.count < 0) { + spanEl.style.color = DIFF_COLOR.GREEN; + } + return spanEl; + }, + cmp(a, b) { + return a.count - b.count; + } + }, + ]; + }, + + buildOptions_() { + const items = []; + for (const isolateEntry of this.isolateEntries_) { + items.push({ + label: isolateEntry.title, + value: isolateEntry + }); + } + this.$.diffOption.style.display = 'inline-block'; + this.selector1_ = createSelector( + this, '', items, this.diffOptionChanged_); + Polymer.dom(this.$.diffOption).appendChild(this.selector1_); + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = ' VS '; + Polymer.dom(this.$.diffOption).appendChild(spanEl); + this.selector2_ = createSelector( + this, '', items, this.diffOptionChanged_); + Polymer.dom(this.$.diffOption).appendChild(this.selector2_); + }, + + diffOptionChanged_() { + const isolateEntry1 = this.selector1_.selectedValue; + const isolateEntry2 = this.selector2_.selectedValue; + if (isolateEntry1 === undefined || isolateEntry2 === undefined) { + return; + } + if (isolateEntry1 === isolateEntry2) { + this.$.diffTable.tableRows = []; + this.$.diffTable.rebuild(); + return; + } + this.$.diffTable.tableRows = [isolateEntry1.diff(isolateEntry2)]; + this.$.diffTable.rebuild(); + }, + + constructTable_() { + this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + this.$.table.tableColumns = [ + { + title: 'Component', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.title; + return typeEl; + }, + showExpandButtons: true + }, + { + title: 'Overall Memory (KB)', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.overall.toFixed(3); + return typeEl; + }, + cmp(a, b) { + return a.overall - b.overall; + } + }, + { + title: 'Over Allocated Memory (KB)', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.overAllocated.toFixed(3); + return typeEl; + }, + cmp(a, b) { + return a.overAllocated - b.overAllocated; + } + }, + { + title: 'Overall Count', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.count; + return typeEl; + }, + cmp(a, b) { + return a.count - b.count; + } + }, + { + title: 'Overall Memory Percent', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.overallPercent.toFixed(3) + '%'; + return typeEl; + }, + cmp(a, b) { + return a.overall - b.overall; + } + }, + { + title: 'Overall Allocated Memory Percent', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.overAllocatedPercent.toFixed(3) + '%'; + return typeEl; + }, + cmp(a, b) { + return a.overAllocated - b.overAllocated; + } + } + ]; + + this.$.table.sortColumnIndex = 1; + this.$.table.sortDescending = true; + }, + + buildSubEntry_(objects, groupEntry, keyToName) { + const typeGroup = INSTANCE_TYPE_GROUPS[groupEntry.title]; + for (const instanceType of typeGroup) { + const e = objects[instanceType]; + if (e === undefined) continue; + delete objects[instanceType]; + let title = instanceType; + if (keyToName !== undefined) title = keyToName(title); + // Represent memery in KB unit. + groupEntry.add(new Entry(title, e.count, e.overall / 1024, + e.over_allocated / 1024, e.histogram, + e.over_allocated_histogram)); + } + }, + + buildUnGroupedEntries_(objects, objectEntry, bucketSize) { + for (const title of Object.getOwnPropertyNames(objects)) { + const obj = objects[title]; + const groupedEntry = new GroupedEntry(title, 0, 0, 0, + new Array(bucketSize), + new Array(bucketSize)); + groupedEntry.setFromObject(obj); + objectEntry.add(groupedEntry); + } + }, + + createGroupEntries_(groupEntries, objects, bucketSize) { + for (const groupName of Object.getOwnPropertyNames( + INSTANCE_TYPE_GROUPS)) { + const groupEntry = new GroupedEntry(groupName, 0, 0, 0, + new Array(bucketSize), + new Array(bucketSize)); + if (INSTANCE_TYPE_GROUPS[groupName].realEntry !== undefined) { + groupEntry.savedRealEntry = + objects[INSTANCE_TYPE_GROUPS[groupName].realEntry]; + delete objects[INSTANCE_TYPE_GROUPS[groupName].realEntry]; + } + groupEntries[groupName] = groupEntry; + } + }, + + buildGroupEntries_(groupEntries, objectEntry) { + for (const groupName of Object.getOwnPropertyNames(groupEntries)) { + const groupEntry = groupEntries[groupName]; + if (groupEntry.savedRealEntry !== undefined) { + groupEntry.setFromObject(groupEntry.savedRealEntry); + groupEntry.accumulateUnknown('UNKNOWN'); + delete groupEntry.savedRealEntry; + } + objectEntry.add(groupEntry); + } + }, + + buildSubEntriesForGroups_(groupEntries, objects) { + for (const instanceType of Object.getOwnPropertyNames(objects)) { + if (IGNORED_ENTRIES.match(instanceType)) { + delete objects[instanceType]; + continue; + } + const e = objects[instanceType]; + for (const name of Object.getOwnPropertyNames(INSTANCE_TYPE_GROUPS)) { + const group = INSTANCE_TYPE_GROUPS[name]; + if (group.match(instanceType)) { + groupEntries[name].add(new Entry( + group.keyToName(instanceType), e.count, e.overall / 1024, + e.over_allocated / 1024, e.histogram, + e.over_allocated_histogram)); + delete objects[instanceType]; + } + } + } + }, + + build_(objects, objectEntry, bucketSize) { + delete objects.END; + const groupEntries = {}; + this.createGroupEntries_(groupEntries, objects, bucketSize); + this.buildSubEntriesForGroups_(groupEntries, objects); + this.buildGroupEntries_(groupEntries, objectEntry); + this.buildUnGroupedEntries_(objects, objectEntry, bucketSize); + }, + + set selection(slices) { + slices.sortEvents(function(a, b) { + return b.start - a.start; + }); + const previous = undefined; + for (const slice of slices) { + if (!slice instanceof tr.e.v8.V8GCStatsThreadSlice) continue; + const liveObjects = slice.liveObjects; + const deadObjects = slice.deadObjects; + const isolate = liveObjects.isolate; + + const isolateEntry = + new GroupedEntry( + 'Isolate_' + isolate + ' at ' + slice.start.toFixed(3) + ' ms', + 0, 0, 0, [], []); + const liveEntry = new GroupedEntry('live objects', 0, 0, 0, [], []); + const deadEntry = new GroupedEntry('dead objects', 0, 0, 0, [], []); + + const liveBucketSize = liveObjects.bucket_sizes.length; + const deadBucketSize = deadObjects.bucket_sizes.length; + + this.build_(tr.b.deepCopy(liveObjects.type_data), liveEntry, + liveBucketSize); + isolateEntry.add(liveEntry); + + this.build_(tr.b.deepCopy(deadObjects.type_data), deadEntry, + deadBucketSize); + isolateEntry.add(deadEntry); + + isolateEntry.calculatePercentage(); + this.isolateEntries_.push(isolateEntry); + } + this.updateTable_(); + + if (slices.length > 1) { + this.buildOptions_(); + this.constructDiffTable_(); + } + }, + + updateTable_() { + this.constructTable_(); + this.$.table.tableRows = this.isolateEntries_; + this.$.table.rebuild(); + }, + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html new file mode 100644 index 00000000000..42d904d94c8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html @@ -0,0 +1,198 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/v8/v8_gc_stats_thread_slice.html"> +<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + + function createModel() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + + m.s1 = m.t2.sliceGroup.pushSlice( + newSliceEx({ + title: 'V8.GC_Objects_Stats', + start: 1, + end: 1, + type: tr.e.v8.V8GCStatsThreadSlice, + cat: 'disabled-by-default-v8.gc_stats', + args: { + // eslint-disable-next-line + live:'{"isolate":"0x00000000001","id":1,"time":111,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":2,"count":3,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":5,"count":6,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"JS_OBJECT_TYPE":{"type":4,"overall":5,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"JS_TYPED_ARRAY_TYPE":{"type":5,"overall":5,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"CODE_TYPE":{"type":4,"overall":6,"count":6,"over_allocated":0,"histogram":[6,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*CODE_BYTECODE_HANDLER":{"type":7,"overall":5,"count":6,"over_allocated":0,"histogram":[6,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*CODE_AGE_Quadragenarian":{"type":8,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}', + // eslint-disable-next-line + dead:'{"isolate":"0x00000000001","id":2,"time":112,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":3,"count":3,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}' + } + }) + ); + m.s2 = m.t2.sliceGroup.pushSlice( + newSliceEx({ + title: 'V8.GC_Objects_Stats', + start: 2, + end: 2, + type: tr.e.v8.V8GCStatsThreadSlice, + cat: 'disabled-by-default-v8.gc_stats', + args: { + // eslint-disable-next-line + live:'{"isolate":"0x00000000001","id":1,"time":113,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":3,"count":4,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":6,"count":7,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":2,"count":2,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}', + // eslint-disable-next-line + dead:'{"isolate":"0x00000000001","id":2,"time":114,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":2,"count":2,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":4,"count":4,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":2,"count":2,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}' + } + }) + ); + m.s3 = m.t2.sliceGroup.pushSlice( + newSliceEx({ + title: 'V8.GC_Objects_Stats', + start: 3, + end: 3, + type: tr.e.v8.V8GCStatsThreadSlice, + cat: 'disabled-by-default-v8.gc_stats', + args: { + // eslint-disable-next-line + live:'{"isolate":"0x00000000001","id":1,"time":115,"bucket_sizes":[32,64,128,256],"type_data":{"FIXED_ARRAY_TYPE":{"type":2,"overall":5,"count":6,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}, "TYPE_DONT_HAVE_GROUP1":{"type":1,"overall":2,"count":3,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}, "TYPE_DONT_HAVE_GROUP2":{"type":1,"overall":2,"count":3,"over_allocated":0,"histogram":[0,1,0,0],"over_allocated_histogram":[0,0,0,0]}}}', + // eslint-disable-next-line + dead:'{"isolate":"0x00000000001","id":2,"time":116,"bucket_sizes":[32,64,128,256],"type_data":{"FIXED_ARRAY_TYPE":{"type":2,"overall":5,"count":6,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}' + } + }) + ); + }); + return m; + } + + test('GCObjectTableSingleSelection', function() { + const m = createModel(); + + const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table'); + const eventSet = new tr.model.EventSet(); + eventSet.push(m.s1); + viewEl.selection = eventSet; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 1); + const row = rows[0]; + assert.strictEqual(row.overall, 0.0263671875); + assert.strictEqual(row.count, 21); + assert.strictEqual(row.overAllocated, 0); + const subRows = row.subRows; + const live = subRows[0]; + assert.strictEqual(live.overall, 0.0224609375); + assert.strictEqual(live.count, 17); + assert.strictEqual(live.overAllocated, 0); + const dead = subRows[1]; + assert.strictEqual(dead.overall, 0.00390625); + assert.strictEqual(dead.count, 4); + assert.strictEqual(dead.overAllocated, 0); + }); + + test('GCObjectTableMultiSelection', function() { + const m = createModel(); + + const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table'); + const eventSet = new tr.model.EventSet(); + eventSet.push(m.s1); + eventSet.push(m.s2); + viewEl.selection = eventSet; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 2); + + let row = rows[0]; + assert.strictEqual(row.overall, 0.0146484375); + assert.strictEqual(row.count, 17); + assert.strictEqual(row.overAllocated, 0); + let subRows = row.subRows; + let live = subRows[0]; + assert.strictEqual(live.overall, 0.0087890625); + assert.strictEqual(live.count, 11); + assert.strictEqual(live.overAllocated, 0); + let dead = subRows[1]; + assert.strictEqual(dead.overall, 0.005859375); + assert.strictEqual(dead.count, 6); + assert.strictEqual(dead.overAllocated, 0); + + row = rows[1]; + assert.strictEqual(row.overall, 0.0263671875); + assert.strictEqual(row.count, 21); + assert.strictEqual(row.overAllocated, 0); + subRows = row.subRows; + live = subRows[0]; + assert.strictEqual(live.overall, 0.0224609375); + assert.strictEqual(live.count, 17); + assert.strictEqual(live.overAllocated, 0); + dead = subRows[1]; + assert.strictEqual(dead.overall, 0.00390625); + assert.strictEqual(dead.count, 4); + assert.strictEqual(dead.overAllocated, 0); + }); + + test('GCObjectTableDiff', function() { + const m = createModel(); + + const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table'); + const eventSet = new tr.model.EventSet(); + eventSet.push(m.s1); + eventSet.push(m.s2); + viewEl.selection = eventSet; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 2); + const diffEntry = rows[0].diff(rows[1]); + + assert.strictEqual(diffEntry.origin.overall.toFixed(3), '0.015'); + assert.strictEqual(diffEntry.origin.overAllocated, 0); + assert.strictEqual(diffEntry.overall.toFixed(3), '-0.004'); + assert.strictEqual(diffEntry.overAllocated, 0); + assert.strictEqual(diffEntry.overallPercent.toFixed(3), '-26.667'); + assert.strictEqual(diffEntry.overAllocatedPercent, 0); + }); + + test('GCObjectTableGroupEntryWithoutGroupDefined', function() { + const m = createModel(); + + const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table'); + const eventSet = new tr.model.EventSet(); + eventSet.push(m.s3); + viewEl.selection = eventSet; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 1); + const row = rows[0]; + assert.strictEqual(row.overall, 0.013671875); + assert.strictEqual(row.count, 18); + assert.strictEqual(row.overAllocated, 0); + const subRows = row.subRows; + + const live = subRows[0]; + assert.strictEqual(live.overall, 0.0087890625); + assert.strictEqual(live.count, 12); + assert.strictEqual(live.overAllocated, 0); + + // ungrouped entry should be top level entry. + const unGrouped1 = live.getEntryFromTitle('TYPE_DONT_HAVE_GROUP1'); + assert.isDefined(unGrouped1); + assert.strictEqual(unGrouped1.overall, 0.001953125); + assert.strictEqual(unGrouped1.count, 3); + assert.strictEqual(unGrouped1.overAllocated, 0); + + const unGrouped2 = live.getEntryFromTitle('TYPE_DONT_HAVE_GROUP2'); + assert.isDefined(unGrouped2); + assert.strictEqual(unGrouped2.overall, 0.001953125); + assert.strictEqual(unGrouped2.count, 3); + assert.strictEqual(unGrouped2.overAllocated, 0); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html new file mode 100644 index 00000000000..e19a367cfca --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html @@ -0,0 +1,181 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/v8/ic_stats_entry.html"> +<link rel="import" href="/tracing/extras/v8/v8_ic_stats_thread_slice.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-e-v8-ic-stats-table'> + <template> + <style> + tr-ui-b-table { + flex: 0 0 auto; + align-self: stretch; + margin-top: 1em; + font-size: 12px; + } + #total { + margin-top: 1em; + margin-left: 0.8em; + } + #groupOption { + display: inline-block; + margin-top: 1em; + margin-left: 0.8em; + } + </style> + <div style="padding-right: 200px"> + <div style="float:right; border-style: solid; border-width: 1px; padding:20px"> + 0 uninitialized<br> + . premonomorphic<br> + 1 monomorphic<br> + ^ recompute handler<br> + P polymorphic<br> + N megamorphic<br> + G generic + </div> + </div> + <div id="total"> + </div> + <div id="groupOption"> + Group Key + </div> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.e.v8', function() { + const PROPERTIES = tr.e.v8.IC_STATS_PROPERTIES.map( + x => {return {label: x, value: x};}); + const ICStatsEntry = tr.e.v8.ICStatsEntry; + const ICStatsEntryGroup = tr.e.v8.ICStatsEntryGroup; + const ICStatsCollection = tr.e.v8.ICStatsCollection; + + Polymer({ + is: 'tr-ui-e-v8-ic-stats-table', + + ready() { + this.icStatsCollection_ = new ICStatsCollection(); + this.groupKey_ = PROPERTIES[0].value; + this.selector_ = tr.ui.b.createSelector(this, 'groupKey', + 'v8ICStatsGroupKey', + this.groupKey_, PROPERTIES); + Polymer.dom(this.$.groupOption).appendChild(this.selector_); + }, + + get groupKey() { + return this.groupKey_; + }, + + set groupKey(key) { + this.groupKey_ = key; + if (this.icStatsCollection_.length === 0) return; + this.updateTable_(this.groupKey_); + }, + + constructTable_(table, groupKey) { + table.tableColumns = [ + { + title: '', + value: row => { + let expanded = false; + const buttonEl = tr.ui.b.createButton('details', function() { + const previousSibling = Polymer.dom(this).parentNode.parentNode; + const parentNode = previousSibling.parentNode; + if (expanded) { + const trEls = parentNode.getElementsByClassName('subTable'); + Array.from(trEls).map(x => x.parentNode.removeChild(x)); + expanded = false; + return; + } + expanded = true; + const subGroups = row.createSubGroup(); + const tr = document.createElement('tr'); + tr.classList.add('subTable'); + tr.appendChild(document.createElement('td')); + const td = document.createElement('td'); + td.colSpan = 3; + for (const subGroup of subGroups) { + const property = subGroup[0]; + const all = Array.from(subGroup[1].values()); + const group = all.slice(0, 20); + const divEl = document.createElement('div'); + const spanEl = document.createElement('span'); + const subTableEl = document.createElement('tr-ui-b-table'); + + spanEl.innerText = `Top 20 out of ${all.length}`; + spanEl.style.fontWeight = 'bold'; + spanEl.style.fontSize = '14px'; + divEl.appendChild(spanEl); + + this.constructTable_(subTableEl, property); + subTableEl.tableRows = group; + subTableEl.rebuild(); + divEl.appendChild(subTableEl); + td.appendChild(divEl); + } + tr.appendChild(td); + parentNode.insertBefore(tr, previousSibling.nextSibling); + }); + return buttonEl; + } + }, + { + title: 'Percentage', + value(row) { + const spanEl = document.createElement('span'); + spanEl.innerText = (row.percentage * 100).toFixed(3) + '%'; + return spanEl; + }, + cmp: (a, b) => a.percentage - b.percentage + }, + { + title: 'Count', + value(row) { + const spanEl = document.createElement('span'); + spanEl.innerText = row.length; + return spanEl; + }, + cmp: (a, b) => a.length - b.length + }, + { + title: groupKey, + value(row) { + const spanEl = document.createElement('span'); + spanEl.innerText = row.key ? row.key : ''; + return spanEl; + } + } + ]; + + table.sortColumnIndex = 1; + table.sortDescending = true; + }, + + updateTable_(groupKey) { + this.constructTable_(this.$.table, groupKey); + this.$.table.tableRows = this.icStatsCollection_.groupBy(groupKey); + this.$.table.rebuild(); + }, + + set selection(slices) { + for (const slice of slices) { + for (const icStatsObj of slice.icStats) { + const entry = new ICStatsEntry(icStatsObj); + this.icStatsCollection_.add(entry); + } + } + this.$.total.innerText = 'Total items: ' + this.icStatsCollection_.length; + this.updateTable_(this.selector_.selectedValue); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html new file mode 100644 index 00000000000..eded2c44a66 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html"> + +<dom-module id='tr-ui-e-multi-v8-gc-stats-thread-slice-sub-view'> + <template> + <style> + </style> + <tr-ui-e-v8-gc-objects-stats-table id="gcObjectsStats"> + </tr-ui-e-v8-gc-objects-stats-table> + </template> +</dom-module> + +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-multi-v8-gc-stats-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.gcObjectsStats.selection = selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-e-multi-v8-gc-stats-thread-slice-sub-view', + tr.e.v8.V8GCStatsThreadSlice, + { + multi: true, + title: 'V8 GC Stats slices' + } +); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html new file mode 100644 index 00000000000..36b14cb7be0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/ic_stats_table.html"> + +<dom-module id='tr-ui-e-multi-v8-ic-stats-thread-slice-sub-view'> + <template> + <tr-ui-e-v8-ic-stats-table id="table"> + </tr-ui-e-v8-ic-stats-table> + </template> +</dom-module> + +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-multi-v8-ic-stats-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.table.selection = selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-e-multi-v8-ic-stats-thread-slice-sub-view', + tr.e.v8.V8ICStatsThreadSlice, + { + multi: true, + title: 'V8 IC stats slices' + } +); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html new file mode 100644 index 00000000000..48c1f05b274 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html"> + +<dom-module id='tr-ui-e-multi-v8-thread-slice-sub-view'> + <template> + <tr-ui-a-multi-thread-slice-sub-view id="content"></tr-ui-a-multi-thread-slice-sub-view> + <tr-ui-e-v8-runtime-call-stats-table id="runtimeCallStats"></tr-ui-e-v8-runtime-call-stats-table> + </template> +</dom-module> + +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-multi-v8-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.runtimeCallStats.slices = selection; + this.$.content.selection = selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-e-multi-v8-thread-slice-sub-view', + tr.e.v8.V8ThreadSlice, + { + multi: true, + title: 'V8 slices' + } +); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html new file mode 100644 index 00000000000..d0d62bed87d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html"> +<link rel="import" href="/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + + function createModel() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + + m.s1 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 0, + end: 10, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + CompileFullCode: [3, 345], + LoadIC_Miss: [5, 567], + ParseLazy: [8, 890] + }}})); + m.s2 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 11, + end: 15, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + HandleApiCall: [1, 123], + OptimizeCode: [7, 789] + }}})); + }); + return m; + } + + test('selectMultiV8ThreadSlices', function() { + const m = createModel(); + + const viewEl = + document.createElement('tr-ui-e-multi-v8-thread-slice-sub-view'); + const selection = new tr.model.EventSet(); + selection.push(m.s1); + selection.push(m.s2); + viewEl.selection = selection; + this.addHTMLOutput(viewEl); + const rows = viewEl.$.runtimeCallStats.$.table.tableRows; + assert.lengthOf(rows, 19); + assert.deepEqual(rows.map(r => r.time), [ + 2714, + 567, + 0, + 789, + 0, + 345, + 0, + 890, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 123 + ]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html new file mode 100644 index 00000000000..27bf0f5ef72 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html @@ -0,0 +1,197 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/v8/runtime_stats_entry.html"> +<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-e-v8-runtime-call-stats-table'> + <template> + <style> + #table, #blink_rcs_table { + flex: 0 0 auto; + align-self: stretch; + margin-top: 1em; + font-size: 12px; + } + + #v8_rcs_heading, #blink_rcs_heading { + padding-top: 1em; + font-size: 18px; + } + </style> + <h1 id="v8_rcs_heading"></h1> + <tr-ui-b-table id="table"></tr-ui-b-table> + <h1 id="blink_rcs_heading"></h1> + <tr-ui-b-table id="blink_rcs_table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.e.v8', function() { + const codeSearchURL_ = 'https://cs.chromium.org/search/?sq=package:chromium&type=cs&q='; + + function removeBlinkPrefix_(name) { + if (name.startsWith('Blink_')) name = name.substring(6); + return name; + } + + function handleCodeSearchForV8_(event) { + if (event.target.parentNode === undefined) return; + let name = event.target.parentNode.entryName; + if (name.startsWith('API_')) name = name.substring(4); + const url = codeSearchURL_ + encodeURIComponent(name) + '+file:src/v8/src'; + window.open(url, '_blank'); + } + + function handleCodeSearchForBlink_(event) { + if (event.target.parentNode === undefined) return; + const name = event.target.parentNode.entryName; + const url = codeSearchURL_ + + encodeURIComponent('RuntimeCallStats::CounterId::k' + name) + + '+file:src/third_party/WebKit/|src/out/Debug/'; + window.open(url, '_blank'); + } + + function createCodeSearchEl_(handleCodeSearch) { + const codeSearchEl = document.createElement('span'); + codeSearchEl.innerText = '?'; + codeSearchEl.style.float = 'right'; + codeSearchEl.style.borderRadius = '5px'; + codeSearchEl.style.backgroundColor = '#EEE'; + codeSearchEl.addEventListener('click', + handleCodeSearch.bind(this)); + return codeSearchEl; + } + + const timeColumn_ = { + title: 'Time', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = (row.time / 1000.0).toFixed(3) + ' ms'; + return typeEl; + }, + width: '100px', + cmp(a, b) { + return a.time - b.time; + } + }; + + const countColumn_ = { + title: 'Count', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.count; + return typeEl; + }, + width: '100px', + cmp(a, b) { + return a.count - b.count; + } + }; + + function percentColumn_(title, totalTime) { + return { + title, + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = (row.time / totalTime * 100).toFixed(3) + '%'; + return typeEl; + }, + width: '100px', + cmp(a, b) { + return a.time - b.time; + } + }; + } + + function nameColumn_(handleCodeSearch, modifyName) { + return { + title: 'Name', + value(row) { + const typeEl = document.createElement('span'); + let name = row.name; + if (modifyName) name = modifyName(name); + typeEl.innerText = name; + if (!(row instanceof tr.e.v8.RuntimeStatsGroup)) { + typeEl.title = 'click ? for code search'; + typeEl.entryName = name; + const codeSearchEl = createCodeSearchEl_(handleCodeSearch); + typeEl.appendChild(codeSearchEl); + } + return typeEl; + }, + width: '200px', + showExpandButtons: true + }; + } + + function initializeCommonOptions_(table) { + table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + table.sortColumnIndex = 1; + table.sortDescending = true; + table.subRowsPropertyName = 'values'; + } + + Polymer({ + is: 'tr-ui-e-v8-runtime-call-stats-table', + + ready() { + this.table_ = this.$.table; + this.blink_rcs_table_ = this.$.blink_rcs_table; + this.totalTime_ = 0; + }, + + constructV8RCSTable_(totalTime) { + this.table_.tableColumns = [ + nameColumn_(handleCodeSearchForV8_), + timeColumn_, + countColumn_, + percentColumn_('Percent', totalTime) + ]; + + initializeCommonOptions_(this.table_); + }, + + constructBlinkRCSTable_(blinkCppTotalTime) { + this.blink_rcs_table_.tableColumns = [ + nameColumn_(handleCodeSearchForBlink_, removeBlinkPrefix_), + timeColumn_, + countColumn_, + percentColumn_('Percent (of \'Blink C++\' + \'API\')', + blinkCppTotalTime) + ]; + + initializeCommonOptions_(this.blink_rcs_table_); + }, + + set slices(slices) { + const runtimeGroupCollection = new tr.e.v8.RuntimeStatsGroupCollection(); + runtimeGroupCollection.addSlices(slices); + if (runtimeGroupCollection.totalTime > 0) { + this.$.v8_rcs_heading.textContent = 'V8 Runtime Call Stats'; + this.constructV8RCSTable_(runtimeGroupCollection.totalTime); + this.table_.tableRows = runtimeGroupCollection.runtimeGroups; + this.table_.rebuild(); + } + + const blinkRCSGroupCollection = + runtimeGroupCollection.blinkRCSGroupCollection; + if (runtimeGroupCollection.blinkCppTotalTime > 0 && + blinkRCSGroupCollection.totalTime > 0) { + this.$.blink_rcs_heading.textContent = 'Blink Runtime Call Stats'; + this.constructBlinkRCSTable_(runtimeGroupCollection.blinkCppTotalTime); + this.blink_rcs_table_.tableRows = blinkRCSGroupCollection.runtimeGroups; + this.blink_rcs_table_.rebuild(); + } + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html new file mode 100644 index 00000000000..06698d03c4a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html @@ -0,0 +1,236 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html"> +<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + const apiObjectGet = [1, 123]; + const functionCallback = [2, 234]; + const compileFullCode = [3, 345]; + const allocateInTargetSpace = [4, 456]; + const loadIcMiss = [5, 567]; + const jsExecution = [6, 678]; + const optimizeCode = [7, 789]; + const parseLazy = [8, 890]; + const handleApiCall = [9, 901]; + const compileBackground = [1, 101]; + const parseBackground = [2, 202]; + const optimizeCodeBackground = [3, 303]; + + function createModel() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + + m.s1 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 0, + end: 10, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + JS_Execution: jsExecution, + HandleApiCall: handleApiCall, + CompileFullCode: compileFullCode, + LoadIC_Miss: loadIcMiss, + ParseLazy: parseLazy, + RecompileConcurrent: optimizeCode, + OptimizeCode: optimizeCode, + FunctionCallback: functionCallback, + AllocateInTargetSpace: allocateInTargetSpace, + API_Object_Get: apiObjectGet, + CompileBackgroundIgnition: compileBackground, + ParseBackgroundFunctionLiteral: parseBackground, + RecompileConcurrent: optimizeCodeBackground + }}})); + m.s2 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 11, + end: 15, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + JS_Execution: jsExecution, + HandleApiCall: handleApiCall, + CompileFullCode: compileFullCode, + LoadIC_Miss: loadIcMiss, + ParseLazy: parseLazy, + OptimizeCode: optimizeCode, + FunctionCallback: functionCallback, + AllocateInTargetSpace: allocateInTargetSpace, + API_Object_Get: apiObjectGet + }}})); + m.s3 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 11, + end: 15, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + LoadIC_LoadCallback: [1, 111], + StoreIC_StoreCallback: [2, 222], + }}})); + }); + return m; + } + + test('SingleSliceSelection', function() { + const m = createModel(); + + const viewEl = document.createElement( + 'tr-ui-e-v8-runtime-call-stats-table'); + viewEl.slices = [m.s1]; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 19); + assert.deepEqual(rows.map(r => r.time), [ + 5589, + loadIcMiss[1], + optimizeCodeBackground[1], + optimizeCode[1], + compileBackground[1], + compileFullCode[1], + parseBackground[1], + parseLazy[1], + functionCallback[1], + apiObjectGet[1], + 0, + 0, + 0, + 0, + 0, + 0, + allocateInTargetSpace[1], + jsExecution[1], + handleApiCall[1] + ]); + }); + + test('MultiSliceSelection', function() { + const m = createModel(); + + const viewEl = document.createElement( + 'tr-ui-e-v8-runtime-call-stats-table'); + viewEl.slices = [m.s1, m.s2]; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 19); + assert.deepEqual(rows.map(r => r.time), [ + 10572, + loadIcMiss[1] * 2, + optimizeCodeBackground[1], + optimizeCode[1] * 2, + compileBackground[1], + compileFullCode[1] * 2, + parseBackground[1], + parseLazy[1] * 2, + functionCallback[1] * 2, + apiObjectGet[1] * 2, + 0, + 0, + 0, + 0, + 0, + 0, + allocateInTargetSpace[1] * 2, + jsExecution[1] * 2, + handleApiCall[1] * 2 + ]); + + assert.deepEqual(rows.map(r => r.entries_.size), [ + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1 + ]); + }); + + test('groupCorrectly', function() { + const m = createModel(); + + const viewEl = document.createElement( + 'tr-ui-e-v8-runtime-call-stats-table'); + viewEl.slices = [m.s3]; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 19); + assert.deepEqual(rows.map(r => r.time), [ + 333, + 333, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ]); + + assert.deepEqual(rows.map(r => r.entries_.size), [ + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html new file mode 100644 index 00000000000..6a8d5f15b73 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html"> + +<dom-module id='tr-ui-e-single-v8-gc-stats-thread-slice-sub-view'> + <template> + <tr-ui-a-single-event-sub-view id="content"></tr-ui-a-single-event-sub-view> + <tr-ui-e-v8-gc-objects-stats-table id="gcObjectsStats"></tr-ui-e-v8-gc-objects-stats-table> + </template> +</dom-module> + +<script> +'use strict'; +Polymer({ + is: 'tr-ui-e-single-v8-gc-stats-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.content.selection = selection; + this.$.gcObjectsStats.selection = selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-e-single-v8-gc-stats-thread-slice-sub-view', + tr.e.v8.V8GCStatsThreadSlice, + { + multi: false, + title: 'V8 GC stats slice' + } +); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html new file mode 100644 index 00000000000..eeaf407eab1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/ic_stats_table.html"> + +<dom-module id='tr-ui-e-single-v8-ic-stats-thread-slice-sub-view'> + <template> + <tr-ui-e-v8-ic-stats-table id="table"> + </tr-ui-e-v8-ic-stats-table> + </template> +</dom-module> + +<script> +'use strict'; +Polymer({ + is: 'tr-ui-e-single-v8-ic-stats-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.table.selection = selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-e-single-v8-ic-stats-thread-slice-sub-view', + tr.e.v8.V8ICStatsThreadSlice, + { + multi: false, + title: 'V8 IC stats slice' + } +); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html new file mode 100644 index 00000000000..a9b1189fc76 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html"> + +<dom-module id='tr-ui-e-single-v8-thread-slice-sub-view'> + <template> + <tr-ui-a-single-thread-slice-sub-view id="content"></tr-ui-a-single-thread-slice-sub-view> + <tr-ui-e-v8-runtime-call-stats-table id="runtimeCallStats"></tr-ui-e-v8-runtime-call-stats-table> + </template> +</dom-module> + +<script> +'use strict'; +Polymer({ + is: 'tr-ui-e-single-v8-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.runtimeCallStats.slices = selection; + this.$.content.selection = selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-e-single-v8-thread-slice-sub-view', + tr.e.v8.V8ThreadSlice, + { + multi: false, + title: 'V8 slice' + } +); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html new file mode 100644 index 00000000000..9e12aa6e044 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html @@ -0,0 +1,114 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + + function createModel() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + + m.s1 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 0, + end: 10, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + CompileFullCode: [3, 345], + LoadIC_Miss: [5, 567], + ParseLazy: [8, 890] + }}})); + m.s2 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 11, + end: 15, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + HandleApiCall: [1, 123], + OptimizeCode: [7, 789] + }}})); + }); + return m; + } + + test('selectV8ThreadSlice', function() { + const m = createModel(); + + const viewEl = + document.createElement('tr-ui-e-single-v8-thread-slice-sub-view'); + const selection1 = new tr.model.EventSet(); + selection1.push(m.s1); + viewEl.selection = selection1; + this.addHTMLOutput(viewEl); + let rows = viewEl.$.runtimeCallStats.$.table.tableRows; + assert.lengthOf(rows, 19); + assert.deepEqual(rows.map(r => r.time), [ + 1802, + 567, + 0, + 0, + 0, + 345, + 0, + 890, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ]); + + const selection2 = new tr.model.EventSet(); + selection2.push(m.s2); + viewEl.selection = selection2; + rows = viewEl.$.runtimeCallStats.$.table.tableRows; + assert.lengthOf(rows, 19); + assert.deepEqual(rows.map(r => r.time), [ + 912, + 0, + 0, + 789, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 123 + ]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html new file mode 100644 index 00000000000..f78005c2b54 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + + +<link rel="import" href="/tracing/extras/v8_config.html"> +<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html"> +<link rel="import" href="/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html"> +<link rel="import" href="/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/find_control.html b/chromium/third_party/catapult/tracing/tracing/ui/find_control.html new file mode 100644 index 00000000000..daaa0f59777 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/find_control.html @@ -0,0 +1,177 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> +<link rel="import" href="/tracing/ui/find_controller.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> + +<dom-module id='tr-ui-find-control'> + <template> + <style> + :host { + -webkit-user-select: none; + display: flex; + position: relative; + } + input { + -webkit-user-select: auto; + background-color: #f8f8f8; + border: 1px solid rgba(0, 0, 0, 0.5); + box-sizing: border-box; + margin: 0; + padding: 0; + width: 170px; + } + input:focus { + background-color: white; + } + tr-ui-b-toolbar-button { + border-left: none; + margin: 0; + } + #hitCount { + left: 0; + opacity: 0.25; + pointer-events: none; + position: absolute; + text-align: right; + top: 2px; + width: 167px; + z-index: 1; + } + #spinner { + visibility: hidden; + width: 8px; + height: 8px; + left: 154px; + pointer-events: none; + position: absolute; + top: 4px; + z-index: 1; + + border: 2px solid transparent; + border-bottom: 2px solid rgba(0, 0, 0, 0.5); + border-right: 2px solid rgba(0, 0, 0, 0.5); + border-radius: 50%; + } + @keyframes spin { 100% { transform: rotate(360deg); } } + </style> + + <input type='text' id='filter' + on-input="filterTextChanged" + on-keydown="filterKeyDown" + on-blur="filterBlur" + on-focus="filterFocus" + on-mouseup="filterMouseUp" /> + <div id="spinner"></div> + <tr-ui-b-toolbar-button on-click="findPrevious"> + ← + </tr-ui-b-toolbar-button> + <tr-ui-b-toolbar-button on-click="findNext"> + → + </tr-ui-b-toolbar-button> + <div id="hitCount">0 of 0</div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-find-control', + + filterKeyDown(e) { + if (e.keyCode === 27) { + const hkc = tr.b.getHotkeyControllerForElement(this); + if (hkc) { + hkc.childRequestsBlur(this); + } else { + this.blur(); + } + e.preventDefault(); + e.stopPropagation(); + return; + } else if (e.keyCode === 13) { + if (e.shiftKey) { + this.findPrevious(); + } else { + this.findNext(); + } + } + }, + + filterBlur(e) { + this.updateHitCountEl(); + }, + + filterFocus(e) { + this.$.filter.select(); + }, + + // Prevent that the input text is deselected after focusing the find + // control with the mouse. + filterMouseUp(e) { + e.preventDefault(); + }, + + get controller() { + return this.controller_; + }, + + set controller(c) { + this.controller_ = c; + this.updateHitCountEl(); + }, + + focus() { + this.$.filter.focus(); + }, + + get hasFocus() { + return this === document.activeElement; + }, + + filterTextChanged() { + Polymer.dom(this.$.hitCount).textContent = ''; + this.$.spinner.style.visibility = 'visible'; + this.$.spinner.style.animation = 'spin 1s linear infinite'; + this.controller.startFiltering(this.$.filter.value).then(function() { + this.$.spinner.style.visibility = 'hidden'; + this.$.spinner.style.animation = ''; + this.updateHitCountEl(); + }.bind(this)); + }, + + findNext() { + if (this.controller) { + this.controller.findNext(); + } + this.updateHitCountEl(); + }, + + findPrevious() { + if (this.controller) { + this.controller.findPrevious(); + } + this.updateHitCountEl(); + }, + + updateHitCountEl() { + if (!this.controller || this.$.filter.value.length === 0) { + Polymer.dom(this.$.hitCount).textContent = ''; + return; + } + + const n = this.controller.filterHits.length; + const i = n === 0 ? -1 : this.controller.currentHitIndex; + Polymer.dom(this.$.hitCount).textContent = (i + 1) + ' of ' + n; + }, + + setText(string) { + this.$.filter.value = string; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/find_control_test.html b/chromium/third_party/catapult/tracing/tracing/ui/find_control_test.html new file mode 100644 index 00000000000..e3af0ec9c6f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/find_control_test.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/find_control.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const ctl = document.createElement('tr-ui-find-control'); + ctl.controller = { + findNext() { }, + findPrevious() { }, + reset() {}, + + filterHits: ['a', 'b'], + + currentHitIndex: 0 + }; + + this.addHTMLOutput(ctl); + }); + + test('updateHitCountEl_twoResults', function() { + const ctl = document.createElement('tr-ui-find-control'); + ctl.controller = { + findNext() { }, + findPrevious() { }, + reset() {}, + + filterHits: ['a', 'b'], + + currentHitIndex: 0 + }; + + this.addHTMLOutput(ctl); + ctl.$.filter.value = 'test'; + ctl.updateHitCountEl(); + assert.strictEqual(ctl.$.hitCount.textContent, '1 of 2'); + }); + + test('updateHitCountEl_emptyFilter', function() { + const ctl = document.createElement('tr-ui-find-control'); + ctl.controller = { + findNext() { }, + findPrevious() { }, + reset() {}, + + filterHits: ['a', 'b'], + + currentHitIndex: 0 + }; + + this.addHTMLOutput(ctl); + ctl.$.filter.value = ''; + ctl.updateHitCountEl(); + assert.strictEqual(ctl.$.hitCount.textContent, ''); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/find_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/find_controller.html new file mode 100644 index 00000000000..926915b19fb --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/find_controller.html @@ -0,0 +1,154 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2012 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/task.html"> +<link rel="import" href="/tracing/core/filter.html"> +<link rel="import" href="/tracing/model/event_set.html"> + +<script> +'use strict'; + +/** + * @fileoverview FindController. + */ +tr.exportTo('tr.ui', function() { + const Task = tr.b.Task; + + function FindController(brushingStateController) { + this.brushingStateController_ = brushingStateController; + this.filterHits_ = []; + this.currentHitIndex_ = -1; + this.activePromise_ = Promise.resolve(); + this.activeTask_ = undefined; + } + + FindController.prototype = { + __proto__: Object.prototype, + + get model() { + return this.brushingStateController_.model; + }, + + get brushingStateController() { + return this.brushingStateController_; + }, + + enqueueOperation_(operation) { + let task; + if (operation instanceof tr.b.Task) { + task = operation; + } else { + task = new tr.b.Task(operation, this); + } + if (this.activeTask_) { + this.activeTask_ = this.activeTask_.enqueue(task); + } else { + // We're enqueuing the first task, schedule it. + this.activeTask_ = task; + this.activePromise_ = Task.RunWhenIdle(this.activeTask_); + this.activePromise_.then(function() { + this.activePromise_ = undefined; + this.activeTask_ = undefined; + }.bind(this)); + } + }, + + /** + * Updates the filter hits based on the provided |filterText|. Returns a + * promise which resolves when |filterHits| has been refreshed. + */ + startFiltering(filterText) { + const sc = this.brushingStateController_; + if (!sc) return; + + // TODO(beaudoin): Cancel anything left in the task queue, without + // invalidating the promise. + this.enqueueOperation_(function() { + this.filterHits_ = []; + this.currentHitIndex_ = -1; + }.bind(this)); + + // Try constructing a UIState from the filterText. + // UIState.fromUserFriendlyString will throw an error only if the string + // is syntactically correct to a UI state string but with invalid values. + // It will return undefined if there is no syntactic match. + let stateFromString; + try { + stateFromString = sc.uiStateFromString(filterText); + } catch (e) { + this.enqueueOperation_(function() { + const overlay = new tr.ui.b.Overlay(); + Polymer.dom(overlay).textContent = e.message; + overlay.title = 'UI State Navigation Error'; + overlay.visible = true; + }); + return this.activePromise_; + } + + if (stateFromString !== undefined) { + this.enqueueOperation_( + sc.navToPosition.bind(this, stateFromString, true)); + } else { + // filterText is not a navString here -- proceed with find and filter. + if (filterText.length === 0) { + this.enqueueOperation_(sc.findTextCleared.bind(sc)); + } else { + const filter = new tr.c.FullTextFilter(filterText); + const filterHitSet = new tr.model.EventSet(); + this.enqueueOperation_(sc.addAllEventsMatchingFilterToSelectionAsTask( + filter, filterHitSet)); + this.enqueueOperation_(function() { + this.filterHits_ = filterHitSet.toArray(); + sc.findTextChangedTo(filterHitSet); + }.bind(this)); + } + } + return this.activePromise_; + }, + + /** + * Returns the most recent filter hits as an array. Call + * |startFiltering| to ensure this is up to date after the filter settings + * have been changed. + */ + get filterHits() { + return this.filterHits_; + }, + + get currentHitIndex() { + return this.currentHitIndex_; + }, + + find_(dir) { + const firstHit = this.currentHitIndex_ === -1; + if (firstHit && dir < 0) { + this.currentHitIndex_ = 0; + } + + const N = this.filterHits.length; + this.currentHitIndex_ = (this.currentHitIndex_ + dir + N) % N; + + if (!this.brushingStateController_) return; + + this.brushingStateController_.findFocusChangedTo( + new tr.model.EventSet(this.filterHits[this.currentHitIndex])); + }, + + findNext() { + this.find_(1); + }, + + findPrevious() { + this.find_(-1); + } + }; + + return { + FindController, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/find_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/find_controller_test.html new file mode 100644 index 00000000000..76f3362899b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/find_controller_test.html @@ -0,0 +1,366 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/task.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/find_controller.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> +<link rel="import" href="/tracing/ui/timeline_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Task = tr.b.Task; + + /* + * Just enough of the BrushingStateController to support the tests below. + */ + function FakeBrushingStateController() { + this.addAllEventsMatchingFilterToSelectionReturnValue = []; + + this.viewport = undefined; + this.model = undefined; + this.selection = new tr.model.EventSet(); + this.findMatches = new tr.model.EventSet(); + } + + FakeBrushingStateController.prototype = { + addAllEventsMatchingFilterToSelectionAsTask(filter, selection) { + return new Task(function() { + const n = this.addAllEventsMatchingFilterToSelectionReturnValue.length; + for (let i = 0; i < n; i++) { + selection.push( + this.addAllEventsMatchingFilterToSelectionReturnValue[i]); + } + }, this); + }, + + uiStateFromString(string) { + return undefined; + }, + + findTextChangedTo(selection) { + this.findMatches = selection; + this.selection = new tr.model.EventSet(); + }, + + findFocusChangedTo(selection) { + this.selection = selection; + }, + + findTextCleared(selection) { + this.selection = new tr.model.EventSet(); + this.findMatches = new tr.model.EventSet(); + } + }; + + test('findControllerNoModel', function() { + const brushingStateController = new FakeBrushingStateController(); + const controller = new tr.ui.FindController(brushingStateController); + controller.findNext(); + controller.findPrevious(); + }); + + test('findControllerEmptyHit', function() { + const brushingStateController = new FakeBrushingStateController(); + const controller = new tr.ui.FindController(brushingStateController); + + brushingStateController.selection = new tr.model.EventSet(); + brushingStateController.findMatches = new tr.model.EventSet(); + controller.findNext(); + assert.lengthOf(brushingStateController.selection, 0); + assert.lengthOf(brushingStateController.findMatches, 0); + controller.findPrevious(); + assert.lengthOf(brushingStateController.selection, 0); + assert.lengthOf(brushingStateController.findMatches, 0); + }); + + test('findControllerOneHit', function() { + const brushingStateController = new FakeBrushingStateController(); + const controller = new tr.ui.FindController(brushingStateController); + + const s1 = {guid: 1}; + brushingStateController.addAllEventsMatchingFilterToSelectionReturnValue = [ + s1 + ]; + return new Promise(function(resolve, reject) { + controller.startFiltering('asdf').then(function() { + try { + assert.lengthOf(brushingStateController.selection, 0); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.findMatches), s1); + + controller.findNext(); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.selection), s1); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.findMatches), s1); + + controller.findNext(); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.selection), s1); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.findMatches), s1); + + controller.findPrevious(); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.selection), s1); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.findMatches), s1); + resolve(); + } catch (err) { + reject(err); + } + }); + }); + }); + + test('findControllerMultipleHits', function() { + const brushingStateController = new FakeBrushingStateController(); + const controller = new tr.ui.FindController(brushingStateController); + + const s1 = {guid: 1}; + const s2 = {guid: 2}; + const s3 = {guid: 3}; + + brushingStateController.addAllEventsMatchingFilterToSelectionReturnValue = [ + s1, s2, s3 + ]; + return new Promise(function(resolve, reject) { + controller.startFiltering('asdf').then(function() { + try { + // Loop through hits then when we wrap, try moving backward. + assert.lengthOf(brushingStateController.selection, 0); + assert.lengthOf(brushingStateController.findMatches, 3); + let matches = Array.from(brushingStateController.findMatches); + assert.strictEqual(matches[0], s1); + assert.strictEqual(matches[1], s2); + assert.strictEqual(matches[2], s3); + + controller.findNext(); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.selection), s1); + + controller.findNext(); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.selection), s2); + + controller.findNext(); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.selection), s3); + + controller.findNext(); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.selection), s1); + + controller.findPrevious(); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.selection), s3); + + controller.findPrevious(); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.selection), s2); + assert.lengthOf(brushingStateController.findMatches, 3); + matches = Array.from(brushingStateController.findMatches); + assert.strictEqual(matches[0], s1); + assert.strictEqual(matches[1], s2); + assert.strictEqual(matches[2], s3); + resolve(); + } catch (err) { + reject(err); + } + }); + }); + }); + + test('findControllerChangeFilterAfterNext', function() { + const brushingStateController = new FakeBrushingStateController(); + const controller = new tr.ui.FindController(brushingStateController); + + const s1 = {guid: 1}; + const s2 = {guid: 2}; + const s3 = {guid: 3}; + const s4 = {guid: 4}; + + brushingStateController.addAllEventsMatchingFilterToSelectionReturnValue = [ + s1, s2, s3 + ]; + return new Promise(function(resolve, reject) { + controller.startFiltering('asdf').then(function() { + // Loop through hits then when we wrap, try moving backward. + controller.findNext(); + brushingStateController. + addAllEventsMatchingFilterToSelectionReturnValue = [s4]; + + controller.startFiltering('asdfsf').then(function() { + controller.findNext(); + try { + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.selection), s4); + resolve(); + } catch (err) { + reject(err); + } + }); + }); + }); + }); + + test('findControllerSelectsAllItemsFirst', function() { + const brushingStateController = new FakeBrushingStateController(); + const controller = new tr.ui.FindController(brushingStateController); + + const s1 = {guid: 1}; + const s2 = {guid: 2}; + const s3 = {guid: 3}; + brushingStateController.addAllEventsMatchingFilterToSelectionReturnValue = [ + s1, s2, s3 + ]; + return new Promise(function(resolve, reject) { + controller.startFiltering('asdfsf').then(function() { + try { + assert.lengthOf(brushingStateController.selection, 0); + assert.lengthOf(brushingStateController.findMatches, 3); + let matches = Array.from(brushingStateController.findMatches); + assert.strictEqual(matches[0], s1); + assert.strictEqual(matches[1], s2); + assert.strictEqual(matches[2], s3); + + controller.findNext(); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.selection), s1); + + controller.findNext(); + assert.strictEqual( + tr.b.getOnlyElement(brushingStateController.selection), s2); + assert.lengthOf(brushingStateController.findMatches, 3); + matches = Array.from(brushingStateController.findMatches); + assert.strictEqual(matches[0], s1); + assert.strictEqual(matches[1], s2); + assert.strictEqual(matches[2], s3); + resolve(); + } catch (err) { + reject(err); + } + }); + }); + }); + + test('findControllerWithRealTimeline', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice(new tr.model.ThreadSlice( + '', 'a', 0, 1, {}, 3)); + model.t1 = t1; + }); + + const container = document.createElement('track-view-container'); + container.id = 'track_view_container'; + + const timeline = document.createElement('tr-ui-timeline-view'); + Polymer.dom(timeline).appendChild(container); + + // This is for testing only, have to make sure things link up right. + timeline.trackViewContainer_ = container; + + timeline.model = model; + + const brushingStateController = timeline.brushingStateController; + const controller = timeline.findCtl_.controller; + + // Test find with no filterText. + controller.findNext(); + + // Test find with filter txt. + return new Promise(function(resolve, reject) { + controller.startFiltering('a').then(function() { + try { + assert.strictEqual(brushingStateController.selection.length, 0); + assert.deepEqual(Array.from(brushingStateController.findMatches), + model.t1.sliceGroup.slices); + + controller.findNext(); + assert.isTrue(brushingStateController.selection.equals( + new tr.model.EventSet(model.t1.sliceGroup.slices[0]))); + + controller.startFiltering('xxx').then(function() { + try { + assert.strictEqual(brushingStateController.findMatches.length, 0); + assert.strictEqual(brushingStateController.selection.length, 1); + + controller.findNext(); + assert.strictEqual(brushingStateController.selection.length, 0); + + controller.findNext(); + assert.strictEqual(brushingStateController.selection.length, 0); + resolve(); + } catch (err) { + reject(err); + } + }); + } catch (err) { + reject(err); + } + }); + }); + }); + + test('findControllerNavigation', function() { + const brushingStateController = new FakeBrushingStateController(); + const controller = new tr.ui.FindController(brushingStateController); + + let navToPositionCallCount = 0; + let findTextClearedCallCount = 0; + const fakeUIState = {}; + brushingStateController.uiStateFromString = function(string) { + if (string === '') return undefined; + + assert.strictEqual(string, '2000@1.2x7'); + return fakeUIState; + }; + brushingStateController.navToPosition = function(uiState) { + assert.strictEqual(uiState, fakeUIState); + navToPositionCallCount++; + }; + brushingStateController.findTextCleared = function() { + findTextClearedCallCount++; + }; + + return new Promise(function(resolve, reject) { + controller.startFiltering('2000@1.2x7').then(function() { + assert.strictEqual(navToPositionCallCount, 1); + }).then(function() { + controller.startFiltering('').then(function() { + try { + assert.strictEqual(findTextClearedCallCount, 1); + resolve(); + } catch (err) { + reject(err); + } + }); + }); + }); + }); + + test('findControllerClearAfterSet', function() { + const brushingStateController = new FakeBrushingStateController(); + const controller = new tr.ui.FindController(brushingStateController); + let findTextChangedToCalled = false; + brushingStateController.findTextChangedTo = function(selection) { + findTextChangedToCalled = true; + }; + brushingStateController.findTextCleared = function() { + assert.strictEqual(findTextChangedToCalled, true); + }; + controller.startFiltering('1'); + controller.startFiltering(''); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-left.png b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-left.png Binary files differnew file mode 100644 index 00000000000..8eef2bf7ecc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-left.png diff --git a/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-mid.png b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-mid.png Binary files differnew file mode 100644 index 00000000000..c67e697de5f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-mid.png diff --git a/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-right.png b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-right.png Binary files differnew file mode 100644 index 00000000000..834004a0f74 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-right.png diff --git a/chromium/third_party/catapult/tracing/tracing/ui/images/ui-states.png b/chromium/third_party/catapult/tracing/tracing/ui/images/ui-states.png Binary files differnew file mode 100644 index 00000000000..83d09179817 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/images/ui-states.png diff --git a/chromium/third_party/catapult/tracing/tracing/ui/metrics_debugger_app.html b/chromium/third_party/catapult/tracing/tracing/ui/metrics_debugger_app.html new file mode 100644 index 00000000000..2f21d5156f8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/metrics_debugger_app.html @@ -0,0 +1,132 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/extras/full_config.html"> +<link rel="import" href="/tracing/importer/import.html"> +<link rel="import" href="/tracing/metrics/all_metrics.html"> +<link rel="import" href="/tracing/metrics/metric_map_function.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/mre/mre_result.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/file.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<dom-module id='tracing-ui-metrics-debugger-app'> + <style> + pre { + overflow: auto; + } + #bar { + display: flex; + flex-direction: row; + padding: 1px 6px; + } + </style> + + <template> + <top-left-controls id="top_left_controls"></top-left-controls> + <input id="load_trace" type="file"/> + <button id="run_metric">Run metric</button> + <div id="trace_info"></div> + <pre id="map_results"> + </pre> + </template> +</dom-module> + +<script> +'use strict'; + +tr.exportTo('tr.ui', function() { + Polymer({ + is: 'tracing-ui-metrics-debugger-app', + created() { + this.metrics_ = []; + tr.metrics.MetricRegistry.getAllRegisteredTypeInfos().forEach( + function(m) { + this.metrics_.push({ + label: m.constructor.name, + value: m.constructor.name + }); + }, this); + this.activeTrace_ = undefined; + this.settingsKey_ = undefined; + this.currentMetricName_ = undefined; + this.settingsKey_ = 'metrics-debugger-app-metric-name'; + }, + + ready() { + const metricSelector = tr.ui.b.createSelector( + this, 'currentMetricName_', + this.settingsKey_, + this.metrics_[0].value, + this.metrics_); + Polymer.dom(this.$.top_left_controls).appendChild( + metricSelector); + + this.$.load_trace.addEventListener('change', function(event) { + const file = event.target.files[0]; + this.onTraceFileSelected_(file); + }.bind(this)); + this.$.run_metric.addEventListener( + 'click', function(event) { + event.stopPropagation(); + this.onRunMetricClicked_(); + }.bind(this)); + }, + + onRunMetricClicked_() { + if (this.activeTrace_ === undefined) { + tr.ui.b.Overlay.showError('You must load a trace first!'); + return; + } + const result = new tr.mre.MreResult(); + const model = this.activeTrace_.model; + const options = {metrics: [this.currentMetricName_]}; + try { + tr.metrics.metricMapFunction(result, model, options); + this.set( + '$.map_results.textContent', + 'Metric result:\n' + JSON.stringify(result.asDict(), undefined, 2)); + } catch (err) { + tr.ui.b.Overlay.showError('Error running metric:\n' + err.stack); + } + }, + + onTraceFileSelected_(file) { + tr.ui.b.readFile(file).then( + function(data) { + this.setActiveTrace(file.name, data); + }.bind(this), + function(err) { + tr.ui.b.Overlay.showError('Error while loading file: ' + err); + }); + }, + + setActiveTrace(filename, data) { + const model = new tr.Model(); + const importOptions = new tr.importer.ImportOptions(); + importOptions.pruneEmptyContainers = false; + importOptions.showImportWarnings = true; + importOptions.trackDetailedModelStats = true; + + const i = new tr.importer.Import(model, importOptions); + i.importTracesWithProgressDialog([data]).then( + function() { + this.activeTrace_ = { + filename, + model, + }; + Polymer.dom(this.$.trace_info).textContent = 'Trace file ' + + filename + ' is loaded.'; + }.bind(this), + function(err) { + tr.ui.b.Overlay.showError('Trace import error: ' + err); + }); + }, + }); + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/null_brushing_state_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/null_brushing_state_controller.html new file mode 100644 index 00000000000..2ab621151db --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/null_brushing_state_controller.html @@ -0,0 +1,196 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/event_target.html"> +<link rel="import" href="/tracing/base/task.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/selection_state.html"> +<link rel="import" href="/tracing/ui/base/ui_state.html"> +<link rel="import" href="/tracing/ui/brushing_state.html"> +<link rel="import" href="/tracing/ui/brushing_state_controller.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui', function() { + /* + * Some elements such as analysis-links require at least one of their + * ancestors to have a BrushingStateController. + * Some clients of such elements, such as histogram-set-view, do not have a + * timeline-view, which is required by the BrushingStateController. + * This class provides the API of BrushingStateController but not the + * implementation, unless there is a real BrushingStateController in the + * owning element's ancestor chain, in which case the implementation is + * delegated to the real BrushingStateController. + */ + class NullBrushingStateController extends tr.c.BrushingStateController { + constructor() { + super(undefined); + this.parentController = undefined; + } + + dispatchChangeEvent_() { + if (this.parentController) this.parentController.dispatchChangeEvent_(); + } + + get model() { + if (!this.parentController) return undefined; + return this.parentController.model; + } + + get trackView() { + if (!this.parentController) return undefined; + return this.parentController.trackView; + } + + get viewport() { + if (!this.parentController) return undefined; + return this.parentController.viewport; + } + + get historyEnabled() { + if (!this.parentController) return undefined; + return this.parentController.historyEnabled; + } + + set historyEnabled(historyEnabled) { + if (this.parentController) { + this.parentController.historyEnabled = historyEnabled; + } + } + + modelWillChange() { + if (this.parentController) this.parentController.modelWillChange(); + } + + modelDidChange() { + if (this.parentController) this.parentController.modelDidChange(); + } + + onUserInitiatedSelectionChange_() { + if (this.parentController) { + this.parentController.onUserInitiatedSelectionChange_(); + } + } + + onPopState_(e) { + if (this.parentController) this.parentController.onPopState_(e); + } + + get selection() { + if (!this.parentController) return undefined; + return this.parentController.selection; + } + + get findMatches() { + if (!this.parentController) return undefined; + return this.parentController.findMatches; + } + + get selectionOfInterest() { + if (!this.parentController) return undefined; + return this.parentController.selectionOfInterest; + } + + get currentBrushingState() { + if (!this.parentController) return undefined; + return this.parentController.currentBrushingState; + } + + set currentBrushingState(newBrushingState) { + if (this.parentController) { + this.parentController.currentBrushingState = newBrushingState; + } + } + + addAllEventsMatchingFilterToSelectionAsTask(filter, selection) { + if (this.parentController) { + this.parentController.addAllEventsMatchingFilterToSelectionAsTask( + filter, selection); + } + } + + findTextChangedTo(allPossibleMatches) { + if (this.parentController) { + this.parentController.findTextChangedTo(allPossibleMatches); + } + } + + findFocusChangedTo(currentFocus) { + if (this.parentController) { + this.parentController.findFocusChangedTo(currentFocus); + } + } + + findTextCleared() { + if (this.parentController) { + this.parentController.findTextCleared(); + } + } + + uiStateFromString(string) { + if (this.parentController) { + this.parentController.uiStateFromString(string); + } + } + + navToPosition(uiState, showNavLine) { + if (this.parentController) { + this.parentController.navToPosition(uiState, showNavLine); + } + } + + changeSelectionFromTimeline(selection) { + if (this.parentController) { + this.parentController.changeSelectionFromTimeline(selection); + } + } + + showScriptControlSelection(selection) { + if (this.parentController) { + this.parentController.showScriptControlSelection(selection); + } + } + + changeSelectionFromRequestSelectionChangeEvent(selection) { + if (this.parentController) { + this.parentController.changeSelectionFromRequestSelectionChangeEvent( + selection); + } + } + + changeAnalysisViewRelatedEvents(eventSet) { + if (this.parentController && (eventSet instanceof tr.model.EventSet)) { + this.parentController.changeAnalysisViewRelatedEvents(eventSet); + } + } + + changeAnalysisLinkHoveredEvents(eventSet) { + if (this.parentController && (eventSet instanceof tr.model.EventSet)) { + this.parentController.changeAnalysisLinkHoveredEvents(eventSet); + } + } + + getViewSpecificBrushingState(viewId) { + if (this.parentController) { + this.parentController.getViewSpecificBrushingState(viewId); + } + } + + changeViewSpecificBrushingState(viewId, newState) { + if (this.parentController) { + this.parentController.changeViewSpecificBrushingState(viewId, newState); + } + } + } + + return { + NullBrushingStateController, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/scripting_control.html b/chromium/third_party/catapult/tracing/tracing/ui/scripting_control.html new file mode 100644 index 00000000000..a91a81e05ec --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/scripting_control.html @@ -0,0 +1,200 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/tquery/tquery.html"> + +<dom-module id='tr-ui-scripting-control'> + <template> + <style> + :host { + flex: 1 1 auto; + } + .root { + font-family: monospace; + cursor: text; + + padding: 2px; + margin: 2px; + border: 1px solid rgba(0, 0, 0, 0.5); + background: white; + + height: 100px; + overflow-y: auto; + + transition-property: opacity, height, padding, margin; + transition-duration: .2s; + transition-timing-function: ease-out; + } + .hidden { + margin-top: 0px; + margin-bottom: 0px; + padding-top: 0px; + padding-bottom: 0px; + height: 0px; + opacity: 0; + } + .focused { + outline: auto 5px -webkit-focus-ring-color; + } + #history { + -webkit-user-select: text; + color: #777; + } + #promptContainer { + display: flex; + } + #promptMark { + width: 1em; + color: #468; + } + #prompt { + flex: 1; + width: 100%; + border: none !important; + background-color: inherit !important; + font: inherit !important; + text-overflow: clip !important; + text-decoration: none !important; + } + #prompt:focus { + outline: none; + } + </style> + + <div id="root" class="root hidden" tabindex="0" + on-focus="onConsoleFocus"> + <div id='history'></div> + <div id='promptContainer'> + <span id='promptMark'>></span> + <input id='prompt' type='text' + on-keypress="promptKeyPress" + on-keydown="promptKeyDown" + on-blur="onConsoleBlur"> + </div> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-scripting-control', + + isEnterKey_(event) { + // Check if in IME. + // Remove keyIdentifier after reference build rolls past M51 when + // KeyboardEvent.key was added. + return event.keyCode !== 229 && + (event.key === 'Enter' || event.keyIdentifier === 'Enter'); + }, + + setFocus_(focused) { + const promptEl = this.$.prompt; + if (focused) { + promptEl.focus(); + Polymer.dom(this.$.root).classList.add('focused'); + // Move cursor to the end of any existing text. + if (promptEl.value.length > 0) { + const sel = window.getSelection(); + sel.collapse( + Polymer.dom(promptEl).firstChild, promptEl.value.length); + } + } else { + promptEl.blur(); + Polymer.dom(this.$.root).classList.remove('focused'); + // Workaround for crbug.com/89026 to ensure the prompt doesn't retain + // keyboard focus. + const parent = promptEl.parentElement; + const nextEl = Polymer.dom(promptEl).nextSibling; + promptEl.remove(); + Polymer.dom(parent).insertBefore(promptEl, nextEl); + } + }, + + onConsoleFocus(e) { + e.stopPropagation(); + this.setFocus_(true); + }, + + onConsoleBlur(e) { + e.stopPropagation(); + this.setFocus_(false); + }, + + promptKeyDown(e) { + e.stopPropagation(); + if (!this.isEnterKey_(e)) return; + + e.preventDefault(); + const promptEl = this.$.prompt; + const command = promptEl.value; + if (command.length === 0) return; + + promptEl.value = ''; + this.addLine_(String.fromCharCode(187) + ' ' + command); + + let result; + try { + result = this.controller_.executeCommand(command); + } catch (e) { + result = e.stack || e.stackTrace; + } + + if (result instanceof tr.e.tquery.TQuery) { + // TODO(skyostil): Show a cool spinner. + result.ready().then(function(selection) { + this.addLine_(selection.length + ' matches'); + this.controller_.brushingStateController. + showScriptControlSelection(selection); + }.bind(this)); + } else { + this.addLine_(result); + } + promptEl.scrollIntoView(); + }, + + addLine_(line) { + const historyEl = this.$.history; + if (historyEl.innerText.length !== 0) { + historyEl.innerText += '\n'; + } + historyEl.innerText += line; + }, + + promptKeyPress(e) { + e.stopPropagation(); + }, + + toggleVisibility() { + const root = this.$.root; + if (!this.visible) { + Polymer.dom(root).classList.remove('hidden'); + this.setFocus_(true); + } else { + Polymer.dom(root).classList.add('hidden'); + this.setFocus_(false); + } + }, + + get hasFocus() { + return this === document.activeElement; + }, + + get visible() { + const root = this.$.root; + return !Polymer.dom(root).classList.contains('hidden'); + }, + + get controller() { + return this.controller_; + }, + + set controller(c) { + this.controller_ = c; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/scripting_control_test.html b/chromium/third_party/catapult/tracing/tracing/ui/scripting_control_test.html new file mode 100644 index 00000000000..69e8b2b9de7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/scripting_control_test.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/scripting_control.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const ctl = document.createElement('tr-ui-scripting-control'); + this.addHTMLOutput(ctl); + ctl.toggleVisibility(); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel.html new file mode 100644 index 00000000000..b2611f4ac3a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel.html @@ -0,0 +1,221 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/base/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/ui/base/grouping_table.html"> +<link rel="import" href="/tracing/ui/base/grouping_table_groupby_picker.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-sp-file-size-stats-side-panel'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + toolbar { + align-items: center; + background-color: rgb(236, 236, 236); + border-bottom: 1px solid #8e8e8e; + display: flex; + flex-direction: row; + flex-direction: row; + flex: 0 0 auto; + font-size: 12px; + padding: 0 10px 0 10px; + } + table-container { + display: flex; + min-height: 0px; + overflow-y: auto; + } + </style> + + <toolbar> + <span><b>Group by:</b></span> + <tr-ui-b-grouping-table-groupby-picker id="picker"> + </tr-ui-b-grouping-table-groupby-picker> + </toolbar> + <table-container> + <tr-ui-b-grouping-table id="table"></tr-ui-b-grouping-table> + </table-container> + </template> +</dom-module> + +<script> +'use strict'; +(function() { + Polymer({ + is: 'tr-ui-sp-file-size-stats-side-panel', + behaviors: [tr.ui.behaviors.SidePanel], + + ready() { + this.model_ = undefined; + this.selection_ = new tr.model.EventSet(); + this.$.picker.settingsKey = 'tr-ui-sp-file-size-stats-side-panel-picker'; + this.$.picker.possibleGroups = [ + { + key: 'phase', label: 'Event Type', + dataFn(eventStat) { return eventStat.phase; } + }, + { + key: 'category', label: 'Category', + dataFn(eventStat) { return eventStat.category; } + }, + { + key: 'title', label: 'Title', + dataFn(eventStat) { return eventStat.title; } + } + ]; + // If the picker did not restore currentGroupKeys from Settings, + // then set default currentGroupKeys. + if (this.$.picker.currentGroupKeys.length === 0) { + this.$.picker.currentGroupKeys = ['phase', 'title']; + } + this.$.picker.addEventListener('current-groups-changed', + this.updateContents_.bind(this)); + }, + + get textLabel() { + return 'File Size Stats'; + }, + + supportsModel(m) { + if (!m) { + return { + supported: false, + reason: 'No stats were collected for this file.' + }; + } + + if (m.stats.allTraceEventStats.length === 0) { + return { + supported: false, + reason: 'No stats were collected for this file.' + }; + } + return { + supported: true + }; + }, + + get model() { + return this.model_; + }, + + set model(model) { + this.model_ = model; + this.updateContents_(); + }, + + get rangeOfInterest() { + return this.rangeOfInterest_; + }, + + set rangeOfInterest(rangeOfInterest) { + this.rangeOfInterest_ = rangeOfInterest; + }, + + get selection() { + return this.selection_; + }, + + set selection(selection) { + this.selection_ = selection; + }, + + createColumns_(stats) { + const columns = [ + { + title: 'Title', + value(row) { + const titleEl = document.createElement('span'); + Polymer.dom(titleEl).textContent = row.title; + titleEl.style.textOverflow = 'ellipsis'; + return titleEl; + }, + cmp(a, b) { + return a.title.localeCompare(b.title); + }, + width: '400px' + }, + { + title: 'Num Events', + align: tr.ui.b.TableFormat.ColumnAlignment.RIGHT, + value(row) { + return row.rowStats.numEvents; + }, + cmp(a, b) { + return a.rowStats.numEvents - b.rowStats.numEvents; + }, + width: '80px' + } + ]; + + if (stats && stats.hasEventSizesinBytes) { + columns.push({ + title: 'Bytes', + value(row) { + const value = new tr.b.Scalar(tr.b.Unit.byName.sizeInBytes, + row.rowStats.totalEventSizeinBytes); + const spanEl = tr.v.ui.createScalarSpan(value); + return spanEl; + }, + cmp(a, b) { + return a.rowStats.totalEventSizeinBytes - + b.rowStats.totalEventSizeinBytes; + }, + width: '80px' + }); + } + return columns; + }, + + updateContents_() { + const table = this.$.table; + + const columns = this.createColumns_(this.model.stats); + table.rowStatsConstructor = function ModelStatsRowStats(row) { + const sum = tr.b.math.Statistics.sum(row.data, function(x) { + return x.numEvents; + }); + const totalEventSizeinBytes = tr.b.math.Statistics.sum(row.data, x => + x.totalEventSizeinBytes + ); + return { + numEvents: sum, + totalEventSizeinBytes + }; + }; + table.tableColumns = columns; + table.sortColumnIndex = 1; + table.sortDescending = true; + table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + + table.groupBy = this.$.picker.currentGroups.map(function(group) { + return group.dataFn; + }); + + if (!this.model) { + table.dataToGroup = []; + } else { + table.dataToGroup = this.model.stats.allTraceEventStats; + } + this.$.table.rebuild(); + } + }); + + tr.ui.side_panel.SidePanelRegistry.register(function() { + return document.createElement('tr-ui-sp-file-size-stats-side-panel'); + }); +})(); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel_test.html new file mode 100644 index 00000000000..a764387427a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel_test.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/side_panel/file_size_stats_side_panel.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const TestUtils = tr.c.TestUtils; + + function createModel(opt_customizeModelCallback) { + return TestUtils.newModel(function(model) { + const modelStats = model.stats; + modelStats.willProcessBasicTraceEvent('X', 'cat1', 'title1'); + modelStats.willProcessBasicTraceEvent('X', 'cat1', 'title1'); + modelStats.willProcessBasicTraceEvent('X', 'cat2', 'title1'); + modelStats.willProcessBasicTraceEvent('X', 'cat2', 'title3'); + modelStats.willProcessBasicTraceEvent('Y', 'cat3', 'title3'); + }); + } + + test('instantiate', function() { + const panel = document.createElement('tr-ui-sp-file-size-stats-side-panel'); + panel.model = createModel(); + panel.style.height = '200px'; + this.addHTMLOutput(panel); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel.html new file mode 100644 index 00000000000..4508a4347a9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel.html @@ -0,0 +1,222 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/metrics/metric_map_function.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/mre/mre_result.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view.html"> + +<dom-module id="tr-ui-sp-metrics-side-panel"> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + div#error { + color: red; + } + #results { + font-size: 12px; + } + </style> + + <top-left-controls id="top_left_controls"></top-left-controls> + + <tr-v-ui-histogram-set-view id="results"></tr-v-ui-histogram-set-view> + + <div id="error"></div> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.ui', function() { + Polymer({ + is: 'tr-ui-sp-metrics-side-panel', + behaviors: [tr.ui.behaviors.SidePanel], + + ready() { + this.model_ = undefined; + + this.rangeOfInterest_ = undefined; + this.metricLatenciesMs_ = []; + + this.metrics_ = []; + tr.metrics.MetricRegistry.getAllRegisteredTypeInfos().forEach( + function(m) { + if (m.constructor.name === 'sampleMetric') return; + + this.metrics_.push({ + label: m.constructor.name, + value: m.constructor.name + }); + }, this); + + this.metrics_.sort((x, y) => x.label.localeCompare(y.label)); + + this.settingsKey_ = 'metrics-side-panel-metric-name'; + this.currentMetricName_ = 'responsivenessMetric'; + const metricSelector = tr.ui.b.createSelector( + this, 'currentMetricName_', + this.settingsKey_, + this.currentMetricName_, + this.metrics_); + Polymer.dom(this.$.top_left_controls).appendChild(metricSelector); + metricSelector.addEventListener('change', + this.onMetricChange_.bind(this)); + this.currentMetricTypeInfo_ = + tr.metrics.MetricRegistry.findTypeInfoWithName( + this.currentMetricName_); + + this.recomputeButton_ = tr.ui.b.createButton( + 'Recompute', this.onRecompute_, this); + Polymer.dom(this.$.top_left_controls).appendChild(this.recomputeButton_); + + this.$.results.addEventListener('display-ready', () => { + this.$.results.style.display = ''; + }); + }, + + async build(model) { + this.model_ = model; + await this.updateContents_(); + }, + + /** + * Return an estimate of how many milliseconds it would take to re-run the + * metric. If the metric has not been run, return undefined. + * + * @return {undefined|number} + */ + get metricLatencyMs() { + return tr.b.math.Statistics.mean(this.metricLatenciesMs_); + }, + + onMetricChange_() { + this.currentMetricTypeInfo_ = + tr.metrics.MetricRegistry.findTypeInfoWithName( + this.currentMetricName_); + this.metricLatenciesMs_ = []; + this.updateContents_(); + }, + + onRecompute_() { + this.updateContents_(); + }, + + get textLabel() { + return 'Metrics'; + }, + + supportsModel(m) { + if (!m) { + return { + supported: false, + reason: 'No model available' + }; + } + + return { + supported: true + }; + }, + + get model() { + return this.model_; + }, + + set model(model) { + this.build(model); + }, + + get selection() { + // Not applicable to metrics. + }, + + set selection(_) { + // Not applicable to metrics. + }, + + /** + * @return {undefined|!tr.b.math.Range} + */ + get rangeOfInterest() { + return this.rangeOfInterest_; + }, + + /** + * This may be called rapidly as the mouse is moved. + * If the metric supportsRangeOfInterest and takes less than 100ms, then it + * will be re-run immediately; otherwise, the Recompute button will be + * enabled. + * + * @param {!tr.b.math.Range} range + */ + set rangeOfInterest(range) { + this.rangeOfInterest_ = range; + + if (this.currentMetricTypeInfo_ && + this.currentMetricTypeInfo_.metadata.supportsRangeOfInterest) { + if ((this.metricLatencyMs === undefined) || + (this.metricLatencyMs < 100)) { + this.updateContents_(); + } else { + this.recomputeButton_.style.background = 'red'; + } + } + }, + + async updateContents_() { + Polymer.dom(this.$.error).textContent = ''; + this.$.results.style.display = 'none'; + + if (!this.model_) { + Polymer.dom(this.$.error).textContent = 'Missing model'; + return; + } + + const options = {metrics: [this.currentMetricName_]}; + + if (this.currentMetricTypeInfo_ && + this.currentMetricTypeInfo_.metadata.supportsRangeOfInterest && + this.rangeOfInterest && + !this.rangeOfInterest.isEmpty) { + options.rangeOfInterest = this.rangeOfInterest; + } + + const startDate = new Date(); + const addFailureCb = failure => { + Polymer.dom(this.$.error).textContent = failure.description; + }; + const histograms = tr.metrics.runMetrics( + this.model_, options, addFailureCb); + + this.metricLatenciesMs_.push(new Date() - startDate); + while (this.metricLatenciesMs_.length > 20) { + this.metricLatenciesMs_.shift(); + } + + this.recomputeButton_.style.background = ''; + + await this.$.results.build(histograms); + } + }); + + tr.ui.side_panel.SidePanelRegistry.register(function() { + return document.createElement('tr-ui-sp-metrics-side-panel'); + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel_test.html new file mode 100644 index 00000000000..dc90ab54930 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel_test.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/ui/side_panel/metrics_side_panel.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createModel() { + const m = tr.c.TestUtils.newModelWithEvents([], { + shiftWorldToZero: false, + pruneContainers: false, + customizeModelCallback(m) { + const browserProcess = m.getOrCreateProcess(1); + const browserMain = browserProcess.getOrCreateThread(2); + browserMain.sliceGroup.beginSlice('cat', 'Task', 0); + browserMain.sliceGroup.endSlice(10); + browserMain.sliceGroup.beginSlice('cat', 'Task', 20); + browserMain.sliceGroup.endSlice(30); + } + }); + return m; + } + + function testMetric(values, model) { + const hist = new tr.v.Histogram('test histogram', tr.b.Unit.byName.count); + hist.addSample(1); + values.addHistogram(hist); + } + + tr.metrics.MetricRegistry.register(testMetric); + + test('instantiateCollapsed', async function() { + const metricsPanel = document.createElement('tr-ui-sp-metrics-side-panel'); + this.addHTMLOutput(metricsPanel); + metricsPanel.currentMetricName_ = 'testMetric'; + await metricsPanel.build(createModel()); + + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate( + metricsPanel, elem => elem.textContent === 'test histogram')); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel.html new file mode 100644 index 00000000000..8b8e2f52b14 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<!-- +Copyright 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/ui.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.behaviors', function() { + const SidePanel = { + + get rangeOfInterest() { + throw new Error('Not implemented'); + }, + + set rangeOfInterest(rangeOfInterest) { + throw new Error('Not implemented'); + }, + + get selection() { + throw new Error('Not implemented'); + }, + + set selection(selection) { + throw new Error('Not implemented'); + }, + + get model() { + throw new Error('Not implemented'); + }, + + set model(model) { + throw new Error('Not implemented'); + }, + + supportsModel(m) { + throw new Error('Not implemented'); + } + }; + + return { + SidePanel, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container.html new file mode 100644 index 00000000000..95be3103873 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container.html @@ -0,0 +1,284 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html"> + +<dom-module id='tr-ui-side-panel-container'> + <template> + <style> + :host { + align-items: stretch; + display: flex; + background-color: white; + } + + :host([expanded]) > #side_panel_drag_handle, + :host([expanded]) > active-panel-container { + flex: 1 1 auto; + border-left: 1px solid black; + display: flex; + } + + :host(:not([expanded])) > #side_panel_drag_handle, + :host(:not([expanded])) > active-panel-container { + display: none; + } + + active-panel-container { + display: flex; + } + + tab-strip { + flex: 0 0 auto; + flex-direction: column; + -webkit-user-select: none; + background-color: rgb(236, 236, 236); + border-left: 1px solid black; + cursor: default; + display: flex; + min-width: 18px; /* workaround for flexbox and writing-mode mixing bug */ + padding: 10px 0 10px 0; + font-size: 12px; + } + + tab-strip > tab-strip-label { + flex-shrink: 0; + -webkit-writing-mode: vertical-rl; + white-space: nowrap; + display: inline; + margin-right: 1px; + min-height: 20px; + padding: 15px 3px 15px 1px; + } + + tab-strip > + tab-strip-label:not([enabled]) { + color: rgb(128, 128, 128); + } + + tab-strip > tab-strip-label[selected] { + background-color: white; + border: 1px solid rgb(163, 163, 163); + border-left: none; + padding: 14px 2px 14px 1px; + } + + #active_panel_container { + overflow: auto; + } + </style> + + <tr-ui-b-drag-handle id="side_panel_drag_handle"></tr-ui-b-drag-handle> + <active-panel-container id='active_panel_container'> + </active-panel-container> + <tab-strip id='tab_strip'></tab-strip> + </template> +</dom-module> +<script> +'use strict'; +Polymer({ + is: 'tr-ui-side-panel-container', + + ready() { + this.activePanelContainer_ = this.$.active_panel_container; + this.tabStrip_ = this.$.tab_strip; + + this.dragHandle_ = this.$.side_panel_drag_handle; + this.dragHandle_.horizontal = false; + this.dragHandle_.target = this.activePanelContainer_; + this.rangeOfInterest_ = new tr.b.math.Range(); + this.brushingStateController_ = undefined; + this.onSelectionChanged_ = this.onSelectionChanged_.bind(this); + this.onModelChanged_ = this.onModelChanged_.bind(this); + }, + + get brushingStateController() { + return this.brushingStateController_; + }, + + set brushingStateController(brushingStateController) { + if (this.brushingStateController) { + this.brushingStateController_.removeEventListener( + 'change', this.onSelectionChanged_); + this.brushingStateController_.removeEventListener( + 'model-changed', this.onModelChanged_); + } + this.brushingStateController_ = brushingStateController; + if (this.brushingStateController) { + this.brushingStateController_.addEventListener( + 'change', this.onSelectionChanged_); + this.brushingStateController_.addEventListener( + 'model-changed', this.onModelChanged_); + if (this.model) { + this.onModelChanged_(); + } + } + }, + + onSelectionChanged_() { + if (this.activePanel) { + this.activePanel.selection = this.selection; + } + }, + + get model() { + return this.brushingStateController_.model; + }, + + onModelChanged_() { + this.activePanelType_ = undefined; + this.updateContents_(); + }, + + get expanded() { + this.hasAttribute('expanded'); + }, + + get activePanel() { + return this.activePanelContainer_.children[0]; + }, + + get activePanelType() { + return this.activePanelType_; + }, + + set activePanelType(panelType) { + if (this.model === undefined) { + throw new Error('Cannot activate panel without a model'); + } + + let panel = undefined; + if (panelType) { + panel = document.createElement(panelType); + } + + if (panel !== undefined && !panel.supportsModel(this.model)) { + throw new Error('Cannot activate panel: does not support this model'); + } + + if (this.activePanelType) { + Polymer.dom(this.getLabelElementForPanelType_( + this.activePanelType)).removeAttribute('selected'); + } + + if (this.activePanelType) { + this.getLabelElementForPanelType_( + this.activePanelType).removeAttribute('selected'); + } + + if (this.activePanel) { + this.activePanelContainer_.removeChild(this.activePanel); + } + + if (panelType === undefined) { + Polymer.dom(this).removeAttribute('expanded'); + this.activePanelType_ = undefined; + return; + } + + Polymer.dom(this.getLabelElementForPanelType_(panelType)). + setAttribute('selected', true); + Polymer.dom(this).setAttribute('expanded', true); + + Polymer.dom(this.activePanelContainer_).appendChild(panel); + panel.rangeOfInterest = this.rangeOfInterest_; + panel.selection = this.selection_; + panel.model = this.model; + + this.activePanelType_ = panelType; + }, + + getPanelTypeForConstructor_(constructor) { + for (let i = 0; i < this.tabStrip_.children.length; i++) { + if (this.tabStrip_.children[i].panelType.constructor === constructor) { + return this.tabStrip_.children[i].panelType; + } + } + }, + + getLabelElementForPanelType_(panelType) { + for (let i = 0; i < this.tabStrip_.children.length; i++) { + if (this.tabStrip_.children[i].panelType === panelType) { + return this.tabStrip_.children[i]; + } + } + return undefined; + }, + + updateContents_() { + const previouslyActivePanelType = this.activePanelType; + + Polymer.dom(this.tabStrip_).textContent = ''; + const supportedPanelTypes = []; + const panelTypeInfos = + tr.ui.side_panel.SidePanelRegistry.getAllRegisteredTypeInfos(); + const unsupportedLabelEls = []; + + for (const panelTypeInfo of panelTypeInfos) { + const labelEl = document.createElement('tab-strip-label'); + const panel = panelTypeInfo.constructor(); + const panelType = panel.tagName; + + Polymer.dom(labelEl).textContent = panel.textLabel; + labelEl.panelType = panelType; + + const supported = panel.supportsModel(this.model); + if (this.model && supported.supported) { + supportedPanelTypes.push(panelType); + Polymer.dom(labelEl).setAttribute('enabled', true); + labelEl.addEventListener('click', function(panelType) { + this.activePanelType = + this.activePanelType === panelType ? undefined : panelType; + }.bind(this, panelType)); + Polymer.dom(this.tabStrip_).appendChild(labelEl); + } else { + if (this.activePanel) { + this.activePanelContainer_.removeChild(this.activePanel); + } + this.removeAttribute('expanded'); + unsupportedLabelEls.push(labelEl); + } + } + + // Labels do not shrink, so when the user drags the analysis-view up, the + // bottom labels are obscured first. + // Append all unsupported panel labels after all supported panel labels so + // that unsupported panel labels are obscured first. + for (const labelEl of unsupportedLabelEls) { + Polymer.dom(this.tabStrip_).appendChild(labelEl); + } + + // Restore the active panel, or collapse + if (previouslyActivePanelType && + supportedPanelTypes.includes(previouslyActivePanelType)) { + this.activePanelType = previouslyActivePanelType; + Polymer.dom(this).setAttribute('expanded', true); + } else { + if (this.activePanel) { + Polymer.dom(this.activePanelContainer_).removeChild(this.activePanel); + } + Polymer.dom(this).removeAttribute('expanded'); + } + }, + + get rangeOfInterest() { + return this.rangeOfInterest_; + }, + + set rangeOfInterest(range) { + if (range === undefined) { + throw new Error('Must not be undefined'); + } + this.rangeOfInterest_ = range; + if (this.activePanel) { + this.activePanel.rangeOfInterest = range; + } + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container_test.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container_test.html new file mode 100644 index 00000000000..a51be620426 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container_test.html @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_container.html"> + +<dom-module id="tr-ui-sp-disabled-side-panel"></dom-module> +<dom-module id="tr-ui-sp-enabled-side-panel"></dom-module> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function FakeBrushingStateController() { + this.addAllEventsMatchingFilterToSelectionReturnValue = []; + + this.viewport = undefined; + this.model = undefined; + this.selection = new tr.model.EventSet(); + this.highlight = new tr.model.EventSet(); + } + + FakeBrushingStateController.prototype = { + addEventListener(name, cb) { + } + }; + + function createModel() { + const m = tr.c.TestUtils.newModelWithEvents([], { + shiftWorldToZero: false, + pruneContainers: false, + customizeModelCallback(m) { + const browserProcess = m.getOrCreateProcess(1); + const browserMain = browserProcess.getOrCreateThread(2); + browserMain.sliceGroup.beginSlice('cat', 'Task', 0); + browserMain.sliceGroup.endSlice(10); + browserMain.sliceGroup.beginSlice('cat', 'Task', 20); + browserMain.sliceGroup.endSlice(30); + } + }); + return m; + } + + Polymer({ + is: 'tr-ui-sp-disabled-test-panel', + behaviors: [tr.ui.behaviors.SidePanel], + supportsModel(m) { + return {supported: false}; + }, + get textLabel() { + return 'Disabled'; + } + }); + + tr.ui.side_panel.SidePanelRegistry.register(function disabled() { + return document.createElement('tr-ui-sp-disabled-test-panel'); + }); + + Polymer({ + is: 'tr-ui-sp-enabled-test-panel', + behaviors: [tr.ui.behaviors.SidePanel], + supportsModel(m) { + return {supported: true}; + }, + get textLabel() { + return 'Enabled'; + }, + }); + + tr.ui.side_panel.SidePanelRegistry.register(function enabled() { + return document.createElement('tr-ui-sp-enabled-test-panel'); + }); + + test('instantiateCollapsed', function() { + const brushingStateController = new FakeBrushingStateController(); + brushingStateController.model = createModel(); + + const container = document.createElement('tr-ui-side-panel-container'); + container.brushingStateController = brushingStateController; + this.addHTMLOutput(container); + + // The Enabled tab should appear first in the tab strip even though the + // disabled side panel was registered first. + // There may be other side panels. + const labels = tr.ui.b.findDeepElementsMatching(container, + 'TAB-STRIP-LABEL').map(e => e.textContent); + assert.isBelow(labels.indexOf('Enabled'), labels.indexOf('Disabled')); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry.html new file mode 100644 index 00000000000..0ec139f2225 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/extension_registry.html"> + +<script> +'use strict'; + +// TODO(charliea): This can probably be cleaned up so that we don't have to +// manually wrap the Polymer element names with a function and +// `document.createElement` at each of the registration sites by creating a +// new "Polymer" registration mode. +tr.exportTo('tr.ui.side_panel', function() { + /** + * SidePanelRegistry is an entity for side panel Polymer elements to register + * on so that they'll render a side panel if the model has the correct data. + * + * Example usage: + * + * SidePanelRegistry.register(function() { + * return document.createElement('my-side-panel'); + * }); + * + * @constructor + */ + function SidePanelRegistry() {} + + const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE); + tr.b.decorateExtensionRegistry(SidePanelRegistry, options); + + return { + SidePanelRegistry, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry_test.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry_test.html new file mode 100644 index 00000000000..c174d5eb005 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry_test.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const SidePanelRegistry = tr.ui.side_panel.SidePanelRegistry; + + const testOptions = { + setUp() { + SidePanelRegistry.pushCleanStateBeforeTest(); + }, + + tearDown() { + SidePanelRegistry.popCleanStateAfterTest(); + }, + }; + + test('register', function() { + SidePanelRegistry.register(function() { + return document.createElement('div'); + }); + SidePanelRegistry.register(function() { + return document.createElement('span'); + }); + + const typeInfos = SidePanelRegistry.getAllRegisteredTypeInfos(); + assert.strictEqual(typeInfos[0].constructor().tagName, 'DIV'); + assert.strictEqual(typeInfos[1].constructor().tagName, 'SPAN'); + assert.lengthOf(typeInfos, 2); + }, testOptions); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform.html new file mode 100644 index 00000000000..2aefa6d6de4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform.html @@ -0,0 +1,117 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui', function() { + function TimelineDisplayTransform(opt_that) { + if (opt_that) { + this.set(opt_that); + return; + } + this.scaleX = 1; + this.panX = 0; + this.panY = 0; + } + + TimelineDisplayTransform.prototype = { + set(that) { + this.scaleX = that.scaleX; + this.panX = that.panX; + this.panY = that.panY; + }, + + clone() { + return new TimelineDisplayTransform(this); + }, + + equals(that) { + let eq = true; + if (that === undefined || that === null) { + return false; + } + eq &= this.panX === that.panX; + eq &= this.panY === that.panY; + eq &= this.scaleX === that.scaleX; + return !!eq; + }, + + almostEquals(that) { + let eq = true; + if (that === undefined || that === null) { + return false; + } + eq &= Math.abs(this.panX - that.panX) < 0.001; + eq &= Math.abs(this.panY - that.panY) < 0.001; + eq &= Math.abs(this.scaleX - that.scaleX) < 0.001; + return !!eq; + }, + + incrementPanXInViewUnits(xDeltaView) { + this.panX += this.xViewVectorToWorld(xDeltaView); + }, + + xPanWorldPosToViewPos(worldX, viewX, viewWidth) { + if (typeof viewX === 'string') { + if (viewX === 'left') { + viewX = 0; + } else if (viewX === 'center') { + viewX = viewWidth / 2; + } else if (viewX === 'right') { + viewX = viewWidth - 1; + } else { + throw new Error('viewX must be left|center|right or number.'); + } + } + this.panX = (viewX / this.scaleX) - worldX; + }, + + xPanWorldBoundsIntoView(worldMin, worldMax, viewWidth) { + if (this.xWorldToView(worldMin) < 0) { + this.xPanWorldPosToViewPos(worldMin, 'left', viewWidth); + } else if (this.xWorldToView(worldMax) > viewWidth) { + this.xPanWorldPosToViewPos(worldMax, 'right', viewWidth); + } + }, + + xSetWorldBounds(worldMin, worldMax, viewWidth) { + const worldWidth = worldMax - worldMin; + const scaleX = viewWidth / worldWidth; + const panX = -worldMin; + this.setPanAndScale(panX, scaleX); + }, + + setPanAndScale(p, s) { + this.scaleX = s; + this.panX = p; + }, + + xWorldToView(x) { + return (x + this.panX) * this.scaleX; + }, + + xWorldVectorToView(x) { + return x * this.scaleX; + }, + + xViewToWorld(x) { + return (x / this.scaleX) - this.panX; + }, + + xViewVectorToWorld(x) { + return x / this.scaleX; + } + }; + + return { + TimelineDisplayTransform, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations.html new file mode 100644 index 00000000000..a632a6dc4fd --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations.html @@ -0,0 +1,175 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/animation.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui', function() { + const kDefaultPanAnimationDurationMs = 100.0; + const lerp = tr.b.math.lerp; + + /** + * Pans a TimelineDisplayTransform by a given amount. + * @constructor + * @extends {tr.ui.b.Animation} + * @param {Number} deltaX The total amount of change to the transform's panX. + * @param {Number} deltaY The total amount of change to the transform's panY. + * @param {Number=} opt_durationMs How long the pan animation should run. + * Defaults to kDefaultPanAnimationDurationMs. + */ + function TimelineDisplayTransformPanAnimation( + deltaX, deltaY, opt_durationMs) { + this.deltaX = deltaX; + this.deltaY = deltaY; + if (opt_durationMs === undefined) { + this.durationMs = kDefaultPanAnimationDurationMs; + } else { + this.durationMs = opt_durationMs; + } + + this.startPanX = undefined; + this.startPanY = undefined; + this.startTimeMs = undefined; + } + + TimelineDisplayTransformPanAnimation.prototype = { + __proto__: tr.ui.b.Animation.prototype, + + get affectsPanY() { + return this.deltaY !== 0; + }, + + canTakeOverFor(existingAnimation) { + return existingAnimation instanceof TimelineDisplayTransformPanAnimation; + }, + + takeOverFor(existing, timestamp, target) { + const remainingDeltaXOnExisting = existing.goalPanX - target.panX; + const remainingDeltaYOnExisting = existing.goalPanY - target.panY; + let remainingTimeOnExisting = timestamp - ( + existing.startTimeMs + existing.durationMs); + remainingTimeOnExisting = Math.max(remainingTimeOnExisting, 0); + + this.deltaX += remainingDeltaXOnExisting; + this.deltaY += remainingDeltaYOnExisting; + this.durationMs += remainingTimeOnExisting; + }, + + start(timestamp, target) { + this.startTimeMs = timestamp; + this.startPanX = target.panX; + this.startPanY = target.panY; + }, + + tick(timestamp, target) { + let percentDone = (timestamp - this.startTimeMs) / this.durationMs; + percentDone = tr.b.math.clamp(percentDone, 0, 1); + + target.panX = lerp(percentDone, this.startPanX, this.goalPanX); + if (this.affectsPanY) { + target.panY = lerp(percentDone, this.startPanY, this.goalPanY); + } + return timestamp >= this.startTimeMs + this.durationMs; + }, + + get goalPanX() { + return this.startPanX + this.deltaX; + }, + + get goalPanY() { + return this.startPanY + this.deltaY; + } + }; + + /** + * Zooms in/out on a specified location in the world. + * + * Zooming in and out is all about keeping the area under the mouse cursor, + * here called the "focal point" in the same place under the zoom. If one + * simply changes the scale, the area under the mouse cursor will change. To + * keep the focal point from moving during the zoom, the pan needs to change + * in order to compensate. Thus, a ZoomTo animation is given both a focal + * point in addition to the amount by which to zoom. + * + * @constructor + * @extends {tr.ui.b.Animation} + * @param {Number} goalFocalPointXWorld The X coordinate in the world which is + * of interest. + * @param {Number} goalFocalPointXView Where on the screen the + * goalFocalPointXWorld should stay centered during the zoom. + * @param {Number} goalFocalPointY Where the panY should be when the zoom + * completes. + * @param {Number} zoomInRatioX The ratio of the current scaleX to the goal + * scaleX. + */ + function TimelineDisplayTransformZoomToAnimation( + goalFocalPointXWorld, + goalFocalPointXView, + goalFocalPointY, + zoomInRatioX, + opt_durationMs) { + this.goalFocalPointXWorld = goalFocalPointXWorld; + this.goalFocalPointXView = goalFocalPointXView; + this.goalFocalPointY = goalFocalPointY; + this.zoomInRatioX = zoomInRatioX; + if (opt_durationMs === undefined) { + this.durationMs = kDefaultPanAnimationDurationMs; + } else { + this.durationMs = opt_durationMs; + } + + this.startTimeMs = undefined; + this.startScaleX = undefined; + this.goalScaleX = undefined; + this.startPanY = undefined; + } + + TimelineDisplayTransformZoomToAnimation.prototype = { + __proto__: tr.ui.b.Animation.prototype, + + get affectsPanY() { + return this.startPanY !== this.goalFocalPointY; + }, + + canTakeOverFor(existingAnimation) { + return false; + }, + + takeOverFor(existingAnimation, timestamp, target) { + this.goalScaleX = target.scaleX * this.zoomInRatioX; + }, + + start(timestamp, target) { + this.startTimeMs = timestamp; + this.startScaleX = target.scaleX; + this.goalScaleX = this.zoomInRatioX * target.scaleX; + this.startPanY = target.panY; + }, + + tick(timestamp, target) { + let percentDone = (timestamp - this.startTimeMs) / this.durationMs; + percentDone = tr.b.math.clamp(percentDone, 0, 1); + + target.scaleX = lerp(percentDone, this.startScaleX, this.goalScaleX); + if (this.affectsPanY) { + target.panY = lerp(percentDone, this.startPanY, this.goalFocalPointY); + } + + target.xPanWorldPosToViewPos( + this.goalFocalPointXWorld, this.goalFocalPointXView); + return timestamp >= this.startTimeMs + this.durationMs; + } + }; + + return { + TimelineDisplayTransformPanAnimation, + TimelineDisplayTransformZoomToAnimation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations_test.html new file mode 100644 index 00000000000..215a8863885 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations_test.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/animation_controller.html"> +<link rel="import" href="/tracing/ui/timeline_display_transform.html"> +<link rel="import" href="/tracing/ui/timeline_display_transform_animations.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform; + const TimelineDisplayTransformPanAnimation = + tr.ui.TimelineDisplayTransformPanAnimation; + const TimelineDisplayTransformZoomToAnimation = + tr.ui.TimelineDisplayTransformZoomToAnimation; + + test('panBasic', function() { + const target = new TimelineDisplayTransform(); + target.cloneAnimationState = function() { + return this.clone(); + }; + + const a = new TimelineDisplayTransformPanAnimation(10, 20, 100); + + const controller = new tr.ui.b.AnimationController(); + controller.target = target; + controller.queueAnimation(a, 0); + + assert.isTrue(a.affectsPanY); + tr.b.forcePendingRAFTasksToRun(50); + assert.isAbove(target.panX, 0); + tr.b.forcePendingRAFTasksToRun(100); + assert.isFalse(controller.hasActiveAnimation); + assert.strictEqual(target.panX, 10); + assert.strictEqual(target.panY, 20); + }); + + test('zoomBasic', function() { + const target = new TimelineDisplayTransform(); + target.panY = 30; + target.cloneAnimationState = function() { + return this.clone(); + }; + + const a = new TimelineDisplayTransformZoomToAnimation(10, 20, 30, 5, 100); + + const controller = new tr.ui.b.AnimationController(); + controller.target = target; + controller.queueAnimation(a, 0); + + assert.isFalse(a.affectsPanY); + tr.b.forcePendingRAFTasksToRun(100); + assert.strictEqual(target.scaleX, 5); + }); + + test('panTakeover', function() { + const target = new TimelineDisplayTransform(); + target.cloneAnimationState = function() { + return this.clone(); + }; + + const b = new TimelineDisplayTransformPanAnimation(10, 0, 100); + const a = new TimelineDisplayTransformPanAnimation(10, 0, 100); + + const controller = new tr.ui.b.AnimationController(); + controller.target = target; + controller.queueAnimation(a, 0); + + tr.b.forcePendingRAFTasksToRun(50); + controller.queueAnimation(b, 50); + + tr.b.forcePendingRAFTasksToRun(100); + assert.isTrue(controller.hasActiveAnimation); + + tr.b.forcePendingRAFTasksToRun(150); + assert.isFalse(controller.hasActiveAnimation); + assert.strictEqual(target.panX, 20); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_test.html new file mode 100644 index 00000000000..d0df289c34e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_test.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/timeline_display_transform.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform; + + test('basics', function() { + const a = new TimelineDisplayTransform(); + a.panX = 0; + a.panY = 0; + a.scaleX = 1; + + const b = new TimelineDisplayTransform(); + b.panX = 10; + b.panY = 0; + b.scaleX = 1; + + assert.isFalse(a.equals(b)); + assert.isFalse(a.almostEquals(b)); + + const c = b.clone(); + assert.isTrue(b.equals(c)); + assert.isTrue(b.almostEquals(c)); + + c.set(a); + assert.isTrue(a.equals(c)); + assert.isTrue(a.almostEquals(c)); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_interest_range.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_interest_range.html new file mode 100644 index 00000000000..36126f898db --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_interest_range.html @@ -0,0 +1,249 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui', function() { + /** + * @constructor + */ + function SnapIndicator(y, height) { + this.y = y; + this.height = height; + } + + /** + * The interesting part of the world. + * + * @constructor + */ + function TimelineInterestRange(vp) { + this.viewport_ = vp; + + this.range_ = new tr.b.math.Range(); + + this.leftSelected_ = false; + this.rightSelected_ = false; + + this.leftSnapIndicator_ = undefined; + this.rightSnapIndicator_ = undefined; + } + + TimelineInterestRange.prototype = { + get isEmpty() { + return this.range_.isEmpty; + }, + + reset() { + this.range_.reset(); + this.leftSelected_ = false; + this.rightSelected_ = false; + this.leftSnapIndicator_ = undefined; + this.rightSnapIndicator_ = undefined; + this.viewport_.dispatchChangeEvent(); + }, + + get min() { + return this.range_.min; + }, + + set min(min) { + this.range_.min = min; + this.viewport_.dispatchChangeEvent(); + }, + + get max() { + return this.range_.max; + }, + + set max(max) { + this.range_.max = max; + this.viewport_.dispatchChangeEvent(); + }, + + set(range) { + this.range_.reset(); + this.range_.addRange(range); + this.viewport_.dispatchChangeEvent(); + }, + + setMinAndMax(min, max) { + this.range_.min = min; + this.range_.max = max; + this.viewport_.dispatchChangeEvent(); + }, + + get range() { + return this.range_.range; + }, + + asRangeObject() { + const range = new tr.b.math.Range(); + range.addRange(this.range_); + return range; + }, + + get leftSelected() { + return this.leftSelected_; + }, + + set leftSelected(leftSelected) { + if (this.leftSelected_ === leftSelected) return; + + this.leftSelected_ = leftSelected; + this.viewport_.dispatchChangeEvent(); + }, + + get rightSelected() { + return this.rightSelected_; + }, + + set rightSelected(rightSelected) { + if (this.rightSelected_ === rightSelected) return; + + this.rightSelected_ = rightSelected; + this.viewport_.dispatchChangeEvent(); + }, + + get leftSnapIndicator() { + return this.leftSnapIndicator_; + }, + + set leftSnapIndicator(leftSnapIndicator) { + this.leftSnapIndicator_ = leftSnapIndicator; + this.viewport_.dispatchChangeEvent(); + }, + + get rightSnapIndicator() { + return this.rightSnapIndicator_; + }, + + set rightSnapIndicator(rightSnapIndicator) { + this.rightSnapIndicator_ = rightSnapIndicator; + this.viewport_.dispatchChangeEvent(); + }, + + draw(ctx, viewLWorld, viewRWorld, viewHeight) { + if (this.range_.isEmpty) return; + + const dt = this.viewport_.currentDisplayTransform; + + const markerLWorld = this.min; + const markerRWorld = this.max; + + const markerLView = Math.round(dt.xWorldToView(markerLWorld)); + const markerRView = Math.round(dt.xWorldToView(markerRWorld)); + + ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; + if (markerLWorld > viewLWorld) { + ctx.fillRect(dt.xWorldToView(viewLWorld), 0, + markerLView, viewHeight); + } + + if (markerRWorld < viewRWorld) { + ctx.fillRect(markerRView, 0, + dt.xWorldToView(viewRWorld), viewHeight); + } + + const pixelRatio = window.devicePixelRatio || 1; + ctx.lineWidth = Math.round(pixelRatio); + if (this.range_.range > 0) { + this.drawLine_(ctx, viewLWorld, viewRWorld, + viewHeight, this.min, this.leftSelected_); + this.drawLine_(ctx, viewLWorld, viewRWorld, + viewHeight, this.max, this.rightSelected_); + } else { + this.drawLine_(ctx, viewLWorld, viewRWorld, + viewHeight, this.min, + this.leftSelected_ || this.rightSelected_); + } + ctx.lineWidth = 1; + }, + + drawLine_(ctx, viewLWorld, viewRWorld, height, ts, selected) { + if (ts < viewLWorld || ts >= viewRWorld) return; + + const dt = this.viewport_.currentDisplayTransform; + const viewX = Math.round(dt.xWorldToView(ts)); + + // Apply subpixel translate to get crisp lines. + // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ + ctx.save(); + ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0); + + ctx.beginPath(); + tr.ui.b.drawLine(ctx, viewX, 0, viewX, height); + if (selected) { + ctx.strokeStyle = 'rgb(255, 0, 0)'; + } else { + ctx.strokeStyle = 'rgb(0, 0, 0)'; + } + ctx.stroke(); + + ctx.restore(); + }, + + drawIndicators(ctx, viewLWorld, viewRWorld) { + if (this.leftSnapIndicator_) { + this.drawIndicator_(ctx, viewLWorld, viewRWorld, + this.range_.min, + this.leftSnapIndicator_, + this.leftSelected_); + } + if (this.rightSnapIndicator_) { + this.drawIndicator_(ctx, viewLWorld, viewRWorld, + this.range_.max, + this.rightSnapIndicator_, + this.rightSelected_); + } + }, + + drawIndicator_(ctx, viewLWorld, viewRWorld, + xWorld, si, selected) { + const dt = this.viewport_.currentDisplayTransform; + + const viewX = Math.round(dt.xWorldToView(xWorld)); + + // Apply subpixel translate to get crisp lines. + // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ + ctx.save(); + ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0); + + const pixelRatio = window.devicePixelRatio || 1; + const viewY = si.y * devicePixelRatio; + const viewHeight = si.height * devicePixelRatio; + const arrowSize = 4 * pixelRatio; + + if (selected) { + ctx.fillStyle = 'rgb(255, 0, 0)'; + } else { + ctx.fillStyle = 'rgb(0, 0, 0)'; + } + tr.ui.b.drawTriangle(ctx, + viewX - arrowSize * 0.75, viewY, + viewX + arrowSize * 0.75, viewY, + viewX, viewY + arrowSize); + ctx.fill(); + tr.ui.b.drawTriangle(ctx, + viewX - arrowSize * 0.75, viewY + viewHeight, + viewX + arrowSize * 0.75, viewY + viewHeight, + viewX, viewY + viewHeight - arrowSize); + ctx.fill(); + + ctx.restore(); + } + }; + + return { + SnapIndicator, + TimelineInterestRange, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view.html new file mode 100644 index 00000000000..f6087abf74c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view.html @@ -0,0 +1,1179 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2012 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/event.html"> +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/base/task.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/filter.html"> +<link rel="import" href="/tracing/model/event.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/x_marker_annotation.html"> +<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> +<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html"> +<link rel="import" href="/tracing/ui/base/timing_tool.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/timeline_display_transform_animations.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> +<link rel="import" href="/tracing/ui/tracks/model_track.html"> +<link rel="import" href="/tracing/ui/tracks/x_axis_track.html"> + +<!-- + Interactive visualizaiton of Model objects based loosely on gantt charts. + Each thread in the Model is given a set of Tracks, one per subrow in the + thread. The TimelineTrackView class acts as a controller, creating the + individual tracks, while Tracks do actual drawing. + + Visually, the TimelineTrackView produces (prettier) visualizations like the + following: + Thread1: AAAAAAAAAA AAAAA + BBBB BB + Thread2: CCCCCC CCCCC +--> +<dom-module id='tr-ui-timeline-track-view'> + <template> + <style> + :host { + flex-direction: column; + display: flex; + position: relative; + } + + :host ::content * { + -webkit-user-select: none; + cursor: default; + } + + #drag_box { + background-color: rgba(0, 0, 255, 0.25); + border: 1px solid rgb(0, 0, 96); + font-size: 75%; + position: fixed; + } + + #hint_text { + position: absolute; + bottom: 6px; + right: 6px; + font-size: 8pt; + } + </style> + <slot></slot> + + <div id='drag_box'></div> + <div id='hint_text'></div> + + <tv-ui-b-hotkey-controller id='hotkey_controller'> + </tv-ui-b-hotkey-controller> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-timeline-track-view', + + ready() { + this.displayTransform_ = new tr.ui.TimelineDisplayTransform(); + this.model_ = undefined; + + this.timelineView_ = undefined; + this.pollIfViewportAttachedInterval_ = undefined; + + this.viewport_ = new tr.ui.TimelineViewport(this); + this.viewportDisplayTransformAtMouseDown_ = undefined; + this.brushingStateController_ = undefined; + + this.rulerTrackContainer_ = + new tr.ui.tracks.DrawingContainer(this.viewport_); + Polymer.dom(this).appendChild(this.rulerTrackContainer_); + this.rulerTrackContainer_.invalidate(); + this.rulerTrackContainer_.style.overflowY = 'hidden'; + this.rulerTrackContainer_.style.flexShrink = '0'; + + this.rulerTrack_ = new tr.ui.tracks.XAxisTrack(this.viewport_); + Polymer.dom(this.rulerTrackContainer_).appendChild(this.rulerTrack_); + + this.upperModelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_); + this.upperModelTrack_.upperMode = true; + Polymer.dom(this.rulerTrackContainer_).appendChild(this.upperModelTrack_); + + this.modelTrackContainer_ = + new tr.ui.tracks.DrawingContainer(this.viewport_); + Polymer.dom(this).appendChild(this.modelTrackContainer_); + this.modelTrackContainer_.style.display = 'block'; + this.modelTrackContainer_.style.flexGrow = '1'; + this.modelTrackContainer_.invalidate(); + + this.viewport_.modelTrackContainer = this.modelTrackContainer_; + + this.modelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_); + Polymer.dom(this.modelTrackContainer_).appendChild(this.modelTrack_); + + this.timingTool_ = new tr.ui.b.TimingTool(this.viewport_, this); + + this.initMouseModeSelector(); + + this.hideDragBox_(); + + this.initHintText_(); + + this.onSelectionChanged_ = this.onSelectionChanged_.bind(this); + + this.onDblClick_ = this.onDblClick_.bind(this); + this.addEventListener('dblclick', this.onDblClick_); + + this.onMouseWheel_ = this.onMouseWheel_.bind(this); + this.addEventListener('mousewheel', this.onMouseWheel_); + + this.onMouseDown_ = this.onMouseDown_.bind(this); + this.addEventListener('mousedown', this.onMouseDown_); + + this.onMouseMove_ = this.onMouseMove_.bind(this); + this.addEventListener('mousemove', this.onMouseMove_); + + this.onTouchStart_ = this.onTouchStart_.bind(this); + this.addEventListener('touchstart', this.onTouchStart_); + + this.onTouchMove_ = this.onTouchMove_.bind(this); + this.addEventListener('touchmove', this.onTouchMove_); + + this.onTouchEnd_ = this.onTouchEnd_.bind(this); + this.addEventListener('touchend', this.onTouchEnd_); + + + this.addHotKeys_(); + + this.mouseViewPosAtMouseDown_ = {x: 0, y: 0}; + this.lastMouseViewPos_ = {x: 0, y: 0}; + + this.lastTouchViewPositions_ = []; + + this.alert_ = undefined; + + this.isPanningAndScanning_ = false; + this.isZooming_ = false; + }, + + initMouseModeSelector() { + this.mouseModeSelector_ = document.createElement( + 'tr-ui-b-mouse-mode-selector'); + this.mouseModeSelector_.targetElement = this; + Polymer.dom(this).appendChild(this.mouseModeSelector_); + + this.mouseModeSelector_.addEventListener('beginpan', + this.onBeginPanScan_.bind(this)); + this.mouseModeSelector_.addEventListener('updatepan', + this.onUpdatePanScan_.bind(this)); + this.mouseModeSelector_.addEventListener('endpan', + this.onEndPanScan_.bind(this)); + + this.mouseModeSelector_.addEventListener('beginselection', + this.onBeginSelection_.bind(this)); + this.mouseModeSelector_.addEventListener('updateselection', + this.onUpdateSelection_.bind(this)); + this.mouseModeSelector_.addEventListener('endselection', + this.onEndSelection_.bind(this)); + + this.mouseModeSelector_.addEventListener('beginzoom', + this.onBeginZoom_.bind(this)); + this.mouseModeSelector_.addEventListener('updatezoom', + this.onUpdateZoom_.bind(this)); + this.mouseModeSelector_.addEventListener('endzoom', + this.onEndZoom_.bind(this)); + + this.mouseModeSelector_.addEventListener('entertiming', + this.timingTool_.onEnterTiming.bind(this.timingTool_)); + this.mouseModeSelector_.addEventListener('begintiming', + this.timingTool_.onBeginTiming.bind(this.timingTool_)); + this.mouseModeSelector_.addEventListener('updatetiming', + this.timingTool_.onUpdateTiming.bind(this.timingTool_)); + this.mouseModeSelector_.addEventListener('endtiming', + this.timingTool_.onEndTiming.bind(this.timingTool_)); + this.mouseModeSelector_.addEventListener('exittiming', + this.timingTool_.onExitTiming.bind(this.timingTool_)); + + const m = tr.ui.b.MOUSE_SELECTOR_MODE; + this.mouseModeSelector_.supportedModeMask = + m.SELECTION | m.PANSCAN | m.ZOOM | m.TIMING; + this.mouseModeSelector_.settingsKey = + 'timelineTrackView.mouseModeSelector'; + this.mouseModeSelector_.setKeyCodeForMode(m.PANSCAN, '2'.charCodeAt(0)); + this.mouseModeSelector_.setKeyCodeForMode(m.SELECTION, '1'.charCodeAt(0)); + this.mouseModeSelector_.setKeyCodeForMode(m.ZOOM, '3'.charCodeAt(0)); + this.mouseModeSelector_.setKeyCodeForMode(m.TIMING, '4'.charCodeAt(0)); + + this.mouseModeSelector_.setModifierForAlternateMode( + m.SELECTION, tr.ui.b.MODIFIER.SHIFT); + this.mouseModeSelector_.setModifierForAlternateMode( + m.PANSCAN, tr.ui.b.MODIFIER.SPACE); + }, + + get brushingStateController() { + return this.brushingStateController_; + }, + + set brushingStateController(brushingStateController) { + if (this.brushingStateController_) { + this.brushingStateController_.removeEventListener('change', + this.onSelectionChanged_); + } + this.brushingStateController_ = brushingStateController; + if (this.brushingStateController_) { + this.brushingStateController_.addEventListener('change', + this.onSelectionChanged_); + } + }, + + set timelineView(view) { + this.timelineView_ = view; + }, + + get processViews() { + return this.modelTrack_.processViews; + }, + + onSelectionChanged_() { + this.showHintText_('Press \'m\' to mark current selection'); + this.viewport_.dispatchChangeEvent(); + }, + + set selection(selection) { + throw new Error('DO NOT CALL THIS'); + }, + + set highlight(highlight) { + throw new Error('DO NOT CALL THIS'); + }, + + detach() { + this.modelTrack_.detach(); + this.upperModelTrack_.detach(); + + if (this.pollIfViewportAttachedInterval_) { + window.clearInterval(this.pollIfViewportAttachedInterval_); + this.pollIfViewportAttachedInterval_ = undefined; + } + this.viewport_.detach(); + }, + + get viewport() { + return this.viewport_; + }, + + get model() { + return this.model_; + }, + + set model(model) { + if (!model) { + throw new Error('Model cannot be undefined'); + } + + const modelInstanceChanged = this.model_ !== model; + this.model_ = model; + this.modelTrack_.model = model; + this.upperModelTrack_.model = model; + + // Set up a reasonable viewport. + if (modelInstanceChanged) { + // The following code uses an interval to detect when the parent element + // is attached to the document. That is a trigger to run the setup + // function and install a resize listener. + this.pollIfViewportAttachedInterval_ = window.setInterval( + this.pollIfViewportAttached_.bind(this), 250); + } + }, + + get hasVisibleContent() { + return this.modelTrack_.hasVisibleContent || + this.upperModelTrack_.hasVisibleContent; + }, + + /** + * Checks whether the parentNode is attached to the document. + * When it is, the method installs the iframe-based resize detection hook + * and then runs setInitialViewport_, if present. + */ + pollIfViewportAttached_() { + if (!this.viewport_.isAttachedToDocumentOrInTestMode || + this.viewport_.clientWidth === 0) { + return; + } + window.addEventListener( + 'resize', this.viewport_.dispatchChangeEvent); + window.clearInterval(this.pollIfViewportAttachedInterval_); + this.pollIfViewportAttachedInterval_ = undefined; + + this.setInitialViewport_(); + }, + + setInitialViewport_() { + // We need the canvas size to be up-to-date at this point. We maybe in + // here before the raf fires, so the size may have not been updated since + // the canvas was resized. + this.modelTrackContainer_.updateCanvasSizeIfNeeded_(); + const w = this.modelTrackContainer_.canvas.width; + + let min; + let range; + + if (this.model_.bounds.isEmpty) { + min = 0; + range = 1000; + } else if (this.model_.bounds.range === 0) { + min = this.model_.bounds.min; + range = 1000; + } else { + min = this.model_.bounds.min; + range = this.model_.bounds.range; + } + + const boost = range * 0.15; + this.displayTransform_.set(this.viewport_.currentDisplayTransform); + this.displayTransform_.xSetWorldBounds( + min - boost, min + range + boost, w); + this.viewport_.setDisplayTransformImmediately(this.displayTransform_); + }, + + /** + * @param {Filter} filter The filter to use for finding matches. + * @param {Selection} selection The selection to add matches to. + * @return {Task} which performs the filtering. + */ + addAllEventsMatchingFilterToSelectionAsTask(filter, selection) { + const modelTrack = this.modelTrack_; + const firstT = modelTrack.addAllEventsMatchingFilterToSelectionAsTask( + filter, selection); + const lastT = firstT.after(function() { + this.upperModelTrack_.addAllEventsMatchingFilterToSelection( + filter, selection); + }, this); + return firstT; + }, + + onMouseMove_(e) { + // Zooming requires the delta since the last mousemove so we need to avoid + // tracking it when the zoom interaction is active. + if (this.isZooming_) return; + + this.storeLastMousePos_(e); + }, + + onTouchStart_(e) { + this.storeLastTouchPositions_(e); + this.focusElements_(); + }, + + onTouchMove_(e) { + e.preventDefault(); + this.onUpdateTransformForTouch_(e); + }, + + onTouchEnd_(e) { + this.storeLastTouchPositions_(e); + this.focusElements_(); + }, + + addHotKeys_() { + this.addKeyDownHotKeys_(); + this.addKeyPressHotKeys_(); + }, + + addKeyPressHotKey(dict) { + dict.eventType = 'keypress'; + dict.useCapture = false; + dict.thisArg = this; + const binding = new tr.ui.b.HotKey(dict); + this.$.hotkey_controller.addHotKey(binding); + }, + + addKeyPressHotKeys_() { + this.addKeyPressHotKey({ + keyCodes: ['w'.charCodeAt(0), ','.charCodeAt(0)], + callback(e) { + this.zoomBy_(1.5, true); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCodes: ['s'.charCodeAt(0), 'o'.charCodeAt(0)], + callback(e) { + this.zoomBy_(1 / 1.5, true); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCode: 'g'.charCodeAt(0), + callback(e) { + this.onGridToggle_(true); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCode: 'G'.charCodeAt(0), + callback(e) { + this.onGridToggle_(false); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCodes: ['W'.charCodeAt(0), '<'.charCodeAt(0)], + callback(e) { + this.zoomBy_(10, true); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCodes: ['S'.charCodeAt(0), 'O'.charCodeAt(0)], + callback(e) { + this.zoomBy_(1 / 10, true); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCode: 'a'.charCodeAt(0), + callback(e) { + this.queueSmoothPan_(this.viewWidth_ * 0.3, 0); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCodes: ['d'.charCodeAt(0), 'e'.charCodeAt(0)], + callback(e) { + this.queueSmoothPan_(this.viewWidth_ * -0.3, 0); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCode: 'A'.charCodeAt(0), + callback(e) { + this.queueSmoothPan_(viewWidth * 0.5, 0); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCode: 'D'.charCodeAt(0), + callback(e) { + this.queueSmoothPan_(viewWidth * -0.5, 0); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCode: '0'.charCodeAt(0), + callback(e) { + this.setInitialViewport_(); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCode: 'f'.charCodeAt(0), + callback(e) { + this.zoomToSelection(); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCode: 'm'.charCodeAt(0), + callback(e) { + this.setCurrentSelectionAsInterestRange_(); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCode: 'p'.charCodeAt(0), + callback(e) { + this.selectPowerSamplesInCurrentTimeRange_(); + e.stopPropagation(); + } + }); + + this.addKeyPressHotKey({ + keyCode: 'h'.charCodeAt(0), + callback(e) { + this.toggleHighDetails_(); + e.stopPropagation(); + } + }); + }, + + get viewWidth_() { + return this.modelTrackContainer_.canvas.clientWidth; + }, + + addKeyDownHotKeys_() { + const addBinding = function(dict) { + dict.eventType = 'keydown'; + dict.useCapture = false; + dict.thisArg = this; + const binding = new tr.ui.b.HotKey(dict); + this.$.hotkey_controller.addHotKey(binding); + }.bind(this); + + addBinding({ + keyCode: 37, // Left arrow. + callback(e) { + const curSel = this.brushingStateController_.selection; + const sel = this.viewport.getShiftedSelection(curSel, -1); + + if (sel) { + this.brushingStateController.changeSelectionFromTimeline(sel); + this.panToSelection(); + } else { + this.queueSmoothPan_(this.viewWidth_ * 0.3, 0); + } + e.preventDefault(); + e.stopPropagation(); + } + }); + + addBinding({ + keyCode: 39, // Right arrow. + callback(e) { + const curSel = this.brushingStateController_.selection; + const sel = this.viewport.getShiftedSelection(curSel, 1); + if (sel) { + this.brushingStateController.changeSelectionFromTimeline(sel); + this.panToSelection(); + } else { + this.queueSmoothPan_(-this.viewWidth_ * 0.3, 0); + } + e.preventDefault(); + e.stopPropagation(); + } + }); + }, + + onDblClick_(e) { + if (this.mouseModeSelector_.mode !== + tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION) { + return; + } + + const curSelection = this.brushingStateController_.selection; + if (!curSelection.length || !tr.b.getOnlyElement(curSelection).title) { + return; + } + + const selection = new tr.model.EventSet(); + const filter = new tr.c.ExactTitleFilter( + tr.b.getOnlyElement(curSelection).title); + this.modelTrack_.addAllEventsMatchingFilterToSelection(filter, + selection); + + this.brushingStateController.changeSelectionFromTimeline(selection); + }, + + onMouseWheel_(e) { + if (!e.altKey) return; + + const delta = e.wheelDelta / 120; + const zoomScale = Math.pow(1.5, delta); + this.zoomBy_(zoomScale); + e.preventDefault(); + }, + + onMouseDown_(e) { + if (this.mouseModeSelector_.mode !== + tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION) { + return; + } + + // Mouse down must start on ruler track for crosshair guide lines to draw. + if (e.target !== this.rulerTrack_) return; + + // Make sure we don't start a selection drag event here. + this.dragBeginEvent_ = undefined; + + // Remove nav string marker if it exists, since we're clearing the + // find control box. + if (this.xNavStringMarker_) { + this.model.removeAnnotation(this.xNavStringMarker_); + this.xNavStringMarker_ = undefined; + } + + const dt = this.viewport_.currentDisplayTransform; + tr.ui.b.trackMouseMovesUntilMouseUp(function(e) { // Mouse move handler. + // If mouse event is on ruler, don't do anything. + if (e.target === this.rulerTrack_) return; + + const relativePosition = this.extractRelativeMousePosition_(e); + const loc = tr.model.Location.fromViewCoordinates( + this.viewport_, relativePosition.x, relativePosition.y); + // Not all points on the timeline represents a valid location. + // ex. process header tracks, letter dot tracks. + if (!loc) return; + + if (this.guideLineAnnotation_ === undefined) { + this.guideLineAnnotation_ = + new tr.model.XMarkerAnnotation(loc.xWorld); + this.model.addAnnotation(this.guideLineAnnotation_); + } else { + this.guideLineAnnotation_.timestamp = loc.xWorld; + this.modelTrackContainer_.invalidate(); + } + + // Set the findcontrol's text to nav string of current state. + const state = new tr.ui.b.UIState(loc, + this.viewport_.currentDisplayTransform.scaleX); + this.timelineView_.setFindCtlText( + state.toUserFriendlyString(this.viewport_)); + }.bind(this), + undefined, // Mouse up handler. + function onKeyUpDuringDrag() { + if (this.dragBeginEvent_) { + this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_, + this.dragBoxXEnd_, this.dragBoxYEnd_); + } + }.bind(this)); + }, + + queueSmoothPan_(viewDeltaX, deltaY) { + const deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld( + viewDeltaX); + const animation = new tr.ui.TimelineDisplayTransformPanAnimation( + deltaX, deltaY); + this.viewport_.queueDisplayTransformAnimation(animation); + }, + + /** + * Zoom in or out on the timeline by the given scale factor. + * @param {Number} scale The scale factor to apply. If <1, zooms out. + * @param {boolean} Whether to change the zoom level smoothly. + */ + zoomBy_(scale, smooth) { + if (scale <= 0) { + return; + } + + smooth = !!smooth; + const vp = this.viewport_; + const pixelRatio = window.devicePixelRatio || 1; + + const goalFocalPointXView = this.lastMouseViewPos_.x * pixelRatio; + const goalFocalPointXWorld = vp.currentDisplayTransform.xViewToWorld( + goalFocalPointXView); + if (smooth) { + const animation = new tr.ui.TimelineDisplayTransformZoomToAnimation( + goalFocalPointXWorld, goalFocalPointXView, + vp.currentDisplayTransform.panY, + scale); + vp.queueDisplayTransformAnimation(animation); + } else { + this.displayTransform_.set(vp.currentDisplayTransform); + this.displayTransform_.scaleX *= scale; + this.displayTransform_.xPanWorldPosToViewPos( + goalFocalPointXWorld, goalFocalPointXView, this.viewWidth_); + vp.setDisplayTransformImmediately(this.displayTransform_); + } + }, + + /** + * Zoom into the current selection. + */ + zoomToSelection() { + if (!this.brushingStateController.selectionOfInterest.length) return; + + const bounds = this.brushingStateController.selectionOfInterest.bounds; + if (!bounds.range) return; + + const worldCenter = bounds.center; + const viewCenter = this.modelTrackContainer_.canvas.width / 2; + const adjustedWorldRange = bounds.range * 1.25; + const newScale = this.modelTrackContainer_.canvas.width / + adjustedWorldRange; + const zoomInRatio = newScale / + this.viewport_.currentDisplayTransform.scaleX; + + const animation = new tr.ui.TimelineDisplayTransformZoomToAnimation( + worldCenter, viewCenter, + this.viewport_.currentDisplayTransform.panY, + zoomInRatio); + this.viewport_.queueDisplayTransformAnimation(animation); + }, + + /** + * Pan the view so the current selection becomes visible. + */ + panToSelection() { + if (!this.brushingStateController.selectionOfInterest.length) return; + + const bounds = this.brushingStateController.selectionOfInterest.bounds; + const worldCenter = bounds.center; + const viewWidth = this.viewWidth_; + + const dt = this.viewport_.currentDisplayTransform; + if (false && !bounds.range) { + if (dt.xWorldToView(bounds.center) < 0 || + dt.xWorldToView(bounds.center) > viewWidth) { + this.displayTransform_.set(dt); + this.displayTransform_.xPanWorldPosToViewPos( + worldCenter, 'center', viewWidth); + const deltaX = this.displayTransform_.panX - dt.panX; + const animation = new tr.ui.TimelineDisplayTransformPanAnimation( + deltaX, 0); + this.viewport_.queueDisplayTransformAnimation(animation); + } + return; + } + + this.displayTransform_.set(dt); + this.displayTransform_.xPanWorldBoundsIntoView( + bounds.min, + bounds.max, + viewWidth); + const deltaX = this.displayTransform_.panX - dt.panX; + const animation = new tr.ui.TimelineDisplayTransformPanAnimation( + deltaX, 0); + this.viewport_.queueDisplayTransformAnimation(animation); + }, + + navToPosition(uiState, showNavLine) { + const location = uiState.location; + const scaleX = uiState.scaleX; + const track = location.getContainingTrack(this.viewport_); + + const worldCenter = location.xWorld; + const viewCenter = this.modelTrackContainer_.canvas.width / 5; + const zoomInRatio = scaleX / + this.viewport_.currentDisplayTransform.scaleX; + + // Vertically scroll so track is in view. + track.scrollIntoViewIfNeeded(); + + // Perform zoom and panX animation. + const animation = new tr.ui.TimelineDisplayTransformZoomToAnimation( + worldCenter, viewCenter, + this.viewport_.currentDisplayTransform.panY, + zoomInRatio); + this.viewport_.queueDisplayTransformAnimation(animation); + + if (!showNavLine) return; + // Add an X Marker Annotation at the specified timestamp. + if (this.xNavStringMarker_) { + this.model.removeAnnotation(this.xNavStringMarker_); + } + this.xNavStringMarker_ = + new tr.model.XMarkerAnnotation(worldCenter); + this.model.addAnnotation(this.xNavStringMarker_); + }, + + selectPowerSamplesInCurrentTimeRange_() { + const selectionBounds = this.brushingStateController_.selection.bounds; + if (this.model.device.powerSeries && !selectionBounds.empty) { + const events = this.model.device.powerSeries.getSamplesWithinRange( + selectionBounds.min, selectionBounds.max); + const selection = new tr.model.EventSet(events); + this.brushingStateController_.changeSelectionFromTimeline(selection); + } + }, + + setCurrentSelectionAsInterestRange_() { + const selectionBounds = this.brushingStateController_.selection.bounds; + if (selectionBounds.empty) { + this.viewport_.interestRange.reset(); + return; + } + + if (this.viewport_.interestRange.min === selectionBounds.min && + this.viewport_.interestRange.max === selectionBounds.max) { + this.viewport_.interestRange.reset(); + } else { + this.viewport_.interestRange.set(selectionBounds); + } + }, + + toggleHighDetails_() { + this.viewport_.highDetails = !this.viewport_.highDetails; + }, + + hideDragBox_() { + this.$.drag_box.style.left = '-1000px'; + this.$.drag_box.style.top = '-1000px'; + this.$.drag_box.style.width = 0; + this.$.drag_box.style.height = 0; + }, + + setDragBoxPosition_(xStart, yStart, xEnd, yEnd) { + const loY = Math.min(yStart, yEnd); + const hiY = Math.max(yStart, yEnd); + const loX = Math.min(xStart, xEnd); + const hiX = Math.max(xStart, xEnd); + const modelTrackRect = this.modelTrack_.getBoundingClientRect(); + const dragRect = {left: loX, top: loY, width: hiX - loX, height: hiY - loY}; + + dragRect.right = dragRect.left + dragRect.width; + dragRect.bottom = dragRect.top + dragRect.height; + + const modelTrackContainerRect = + this.modelTrackContainer_.getBoundingClientRect(); + const clipRect = { + left: modelTrackContainerRect.left, + top: modelTrackContainerRect.top, + right: modelTrackContainerRect.right, + bottom: modelTrackContainerRect.bottom + }; + + const headingWidth = window.getComputedStyle( + Polymer.dom(this).querySelector('tr-ui-b-heading')).width; + const trackTitleWidth = parseInt(headingWidth); + clipRect.left = clipRect.left + trackTitleWidth; + + const intersectRect_ = function(r1, r2) { + if (r2.left > r1.right || r2.right < r1.left || + r2.top > r1.bottom || r2.bottom < r1.top) { + return false; + } + + const results = {}; + results.left = Math.max(r1.left, r2.left); + results.top = Math.max(r1.top, r2.top); + results.right = Math.min(r1.right, r2.right); + results.bottom = Math.min(r1.bottom, r2.bottom); + results.width = results.right - results.left; + results.height = results.bottom - results.top; + return results; + }; + + // TODO(dsinclair): intersectRect_ can return false (which should actually + // be undefined) but we use finalDragBox without checking the return value + // which could potentially blowup. Fix this ..... + const finalDragBox = intersectRect_(clipRect, dragRect); + + this.$.drag_box.style.left = finalDragBox.left + 'px'; + this.$.drag_box.style.width = finalDragBox.width + 'px'; + this.$.drag_box.style.top = finalDragBox.top + 'px'; + this.$.drag_box.style.height = finalDragBox.height + 'px'; + this.$.drag_box.style.whiteSpace = 'nowrap'; + + const pixelRatio = window.devicePixelRatio || 1; + const canv = this.modelTrackContainer_.canvas; + const dt = this.viewport_.currentDisplayTransform; + const loWX = dt.xViewToWorld( + (loX - canv.offsetLeft) * pixelRatio); + const hiWX = dt.xViewToWorld( + (hiX - canv.offsetLeft) * pixelRatio); + + Polymer.dom(this.$.drag_box).textContent = + tr.b.Unit.byName.timeDurationInMs.format(hiWX - loWX); + + const e = new tr.b.Event('selectionChanging'); + e.loWX = loWX; + e.hiWX = hiWX; + this.dispatchEvent(e); + }, + + onGridToggle_(left) { + const selection = this.brushingStateController_.selection; + const tb = left ? selection.bounds.min : selection.bounds.max; + + // Toggle the grid off if the grid is on, the marker position is the same + // and the same element is selected (same timebase). + if (this.viewport_.gridEnabled && + this.viewport_.gridSide === left && + this.viewport_.gridInitialTimebase === tb) { + this.viewport_.gridside = undefined; + this.viewport_.gridEnabled = false; + this.viewport_.gridInitialTimebase = undefined; + return; + } + + // Shift the timebase left until its just left of model_.bounds.min. + const numIntervalsSinceStart = Math.ceil((tb - this.model_.bounds.min) / + this.viewport_.gridStep_); + + this.viewport_.gridEnabled = true; + this.viewport_.gridSide = left; + this.viewport_.gridInitialTimebase = tb; + this.viewport_.gridTimebase = tb - + (numIntervalsSinceStart + 1) * this.viewport_.gridStep_; + }, + + storeLastMousePos_(e) { + this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e); + }, + + storeLastTouchPositions_(e) { + this.lastTouchViewPositions_ = this.extractRelativeTouchPositions_(e); + }, + + extractRelativeMousePosition_(e) { + const canv = this.modelTrackContainer_.canvas; + return { + x: e.clientX - canv.offsetLeft, + y: e.clientY - canv.offsetTop + }; + }, + + extractRelativeTouchPositions_(e) { + const canv = this.modelTrackContainer_.canvas; + + const touches = []; + for (let i = 0; i < e.touches.length; ++i) { + touches.push({ + x: e.touches[i].clientX - canv.offsetLeft, + y: e.touches[i].clientY - canv.offsetTop + }); + } + return touches; + }, + + storeInitialMouseDownPos_(e) { + const position = this.extractRelativeMousePosition_(e); + + this.mouseViewPosAtMouseDown_.x = position.x; + this.mouseViewPosAtMouseDown_.y = position.y; + }, + + focusElements_() { + this.$.hotkey_controller.childRequestsGeneralFocus(this); + }, + + storeInitialInteractionPositionsAndFocus_(e) { + this.storeInitialMouseDownPos_(e); + this.storeLastMousePos_(e); + + this.focusElements_(); + }, + + onBeginPanScan_(e) { + const vp = this.viewport_; + this.viewportDisplayTransformAtMouseDown_ = + vp.currentDisplayTransform.clone(); + this.isPanningAndScanning_ = true; + + this.storeInitialInteractionPositionsAndFocus_(e); + e.preventDefault(); + }, + + onUpdatePanScan_(e) { + if (!this.isPanningAndScanning_) return; + + const viewWidth = this.viewWidth_; + + const pixelRatio = window.devicePixelRatio || 1; + const xDeltaView = pixelRatio * (this.lastMouseViewPos_.x - + this.mouseViewPosAtMouseDown_.x); + + const yDelta = this.lastMouseViewPos_.y - + this.mouseViewPosAtMouseDown_.y; + + this.displayTransform_.set(this.viewportDisplayTransformAtMouseDown_); + this.displayTransform_.incrementPanXInViewUnits(xDeltaView); + this.displayTransform_.panY -= yDelta; + this.viewport_.setDisplayTransformImmediately(this.displayTransform_); + + e.preventDefault(); + e.stopPropagation(); + + this.storeLastMousePos_(e); + }, + + onEndPanScan_(e) { + this.isPanningAndScanning_ = false; + + this.storeLastMousePos_(e); + + if (!e.isClick) { + e.preventDefault(); + } + }, + + onBeginSelection_(e) { + const canv = this.modelTrackContainer_.canvas; + const rect = this.modelTrack_.getBoundingClientRect(); + const canvRect = canv.getBoundingClientRect(); + + const inside = rect && + e.clientX >= rect.left && + e.clientX < rect.right && + e.clientY >= rect.top && + e.clientY < rect.bottom && + e.clientX >= canvRect.left && + e.clientX < canvRect.right; + + if (!inside) return; + + this.dragBeginEvent_ = e; + + this.storeInitialInteractionPositionsAndFocus_(e); + e.preventDefault(); + }, + + onUpdateSelection_(e) { + if (!this.dragBeginEvent_) return; + + // Update the drag box + this.dragBoxXStart_ = this.dragBeginEvent_.clientX; + this.dragBoxXEnd_ = e.clientX; + this.dragBoxYStart_ = this.dragBeginEvent_.clientY; + this.dragBoxYEnd_ = e.clientY; + this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_, + this.dragBoxXEnd_, this.dragBoxYEnd_); + }, + + onEndSelection_(e) { + e.preventDefault(); + + if (!this.dragBeginEvent_) return; + + // Stop the dragging. + this.hideDragBox_(); + const eDown = this.dragBeginEvent_; + this.dragBeginEvent_ = undefined; + + // Figure out extents of the drag. + const loY = Math.min(eDown.clientY, e.clientY); + const hiY = Math.max(eDown.clientY, e.clientY); + const loX = Math.min(eDown.clientX, e.clientX); + const hiX = Math.max(eDown.clientX, e.clientX); + + // Convert to worldspace. + const canv = this.modelTrackContainer_.canvas; + const worldOffset = canv.getBoundingClientRect().left; + const loVX = loX - worldOffset; + const hiVX = hiX - worldOffset; + + // Figure out what has been selected. + const selection = new tr.model.EventSet(); + if (eDown.appendSelection) { + const previousSelection = this.brushingStateController_.selection; + if (previousSelection !== undefined) { + selection.addEventSet(previousSelection); + } + } + this.modelTrack_.addIntersectingEventsInRangeToSelection( + loVX, hiVX, loY, hiY, selection); + + // Activate the new selection. + this.brushingStateController_.changeSelectionFromTimeline(selection); + }, + + onBeginZoom_(e) { + this.isZooming_ = true; + + this.storeInitialInteractionPositionsAndFocus_(e); + e.preventDefault(); + }, + + onUpdateZoom_(e) { + if (!this.isZooming_) return; + + const newPosition = this.extractRelativeMousePosition_(e); + + const zoomScaleValue = 1 + (this.lastMouseViewPos_.y - + newPosition.y) * 0.01; + + this.zoomBy_(zoomScaleValue, false); + this.storeLastMousePos_(e); + }, + + onEndZoom_(e) { + this.isZooming_ = false; + + if (!e.isClick) { + e.preventDefault(); + } + }, + + computeTouchCenter_(positions) { + let xSum = 0; + let ySum = 0; + for (let i = 0; i < positions.length; ++i) { + xSum += positions[i].x; + ySum += positions[i].y; + } + return { + x: xSum / positions.length, + y: ySum / positions.length + }; + }, + + computeTouchSpan_(positions) { + let xMin = Number.MAX_VALUE; + let yMin = Number.MAX_VALUE; + let xMax = Number.MIN_VALUE; + let yMax = Number.MIN_VALUE; + for (let i = 0; i < positions.length; ++i) { + xMin = Math.min(xMin, positions[i].x); + yMin = Math.min(yMin, positions[i].y); + xMax = Math.max(xMax, positions[i].x); + yMax = Math.max(yMax, positions[i].y); + } + return Math.sqrt((xMin - xMax) * (xMin - xMax) + + (yMin - yMax) * (yMin - yMax)); + }, + + onUpdateTransformForTouch_(e) { + const newPositions = this.extractRelativeTouchPositions_(e); + const currentPositions = this.lastTouchViewPositions_; + + const newCenter = this.computeTouchCenter_(newPositions); + const currentCenter = this.computeTouchCenter_(currentPositions); + + const newSpan = this.computeTouchSpan_(newPositions); + const currentSpan = this.computeTouchSpan_(currentPositions); + + const vp = this.viewport_; + const viewWidth = this.viewWidth_; + const pixelRatio = window.devicePixelRatio || 1; + + const xDelta = pixelRatio * (newCenter.x - currentCenter.x); + const yDelta = newCenter.y - currentCenter.y; + const zoomScaleValue = currentSpan > 10 ? newSpan / currentSpan : 1; + + const viewFocus = pixelRatio * newCenter.x; + const worldFocus = vp.currentDisplayTransform.xViewToWorld(viewFocus); + + this.displayTransform_.set(vp.currentDisplayTransform); + this.displayTransform_.scaleX *= zoomScaleValue; + this.displayTransform_.xPanWorldPosToViewPos( + worldFocus, viewFocus, viewWidth); + this.displayTransform_.incrementPanXInViewUnits(xDelta); + this.displayTransform_.panY -= yDelta; + vp.setDisplayTransformImmediately(this.displayTransform_); + this.storeLastTouchPositions_(e); + }, + + initHintText_() { + this.$.hint_text.style.display = 'none'; + + this.pendingHintTextClearTimeout_ = undefined; + }, + + showHintText_(text) { + if (this.pendingHintTextClearTimeout_) { + window.clearTimeout(this.pendingHintTextClearTimeout_); + this.pendingHintTextClearTimeout_ = undefined; + } + this.pendingHintTextClearTimeout_ = setTimeout( + this.hideHintText_.bind(this), 1000); + Polymer.dom(this.$.hint_text).textContent = text; + this.$.hint_text.style.display = ''; + }, + + hideHintText_() { + this.pendingHintTextClearTimeout_ = undefined; + this.$.hint_text.style.display = 'none'; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view_test.html new file mode 100644 index 00000000000..a84addbe9bc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view_test.html @@ -0,0 +1,200 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/task.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const EventSet = tr.model.EventSet; + const SelectionState = tr.model.SelectionState; + const Task = tr.b.Task; + + test('instantiate', function() { + const numThreads = 500; + const model = tr.c.TestUtils.newModelWithEvents([], { + shiftWorldToZero: false, + pruneContainers: false, + customizeModelCallback(model) { + const p100 = model.getOrCreateProcess(100); + for (let i = 0; i < numThreads; i++) { + const t = p100.getOrCreateThread(101 + i); + if (i % 2 === 0) { + t.sliceGroup.beginSlice('cat', 'a', 100); + t.sliceGroup.endSlice(110); + } else { + t.sliceGroup.beginSlice('cat', 'b', 50); + t.sliceGroup.endSlice(120); + } + } + } + }); + + const timeline = document.createElement('tr-ui-timeline-track-view'); + timeline.model = model; + timeline.style.maxHeight = '600px'; + this.addHTMLOutput(timeline); + }); + + test('addAllEventsMatchingFilterToSelectionAsTask', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'b', 0, 1.1, {}, 2.8)); + + const t1asg = t1.asyncSliceGroup; + t1asg.slices.push( + tr.c.TestUtils.newAsyncSliceNamed('a', 0, 1, t1, t1)); + t1asg.slices.push( + tr.c.TestUtils.newAsyncSliceNamed('b', 1, 2, t1, t1)); + + const timeline = document.createElement('tr-ui-timeline-track-view'); + timeline.model = model; + + let expected = new tr.model.EventSet( + [t1asg.slices[0], t1.sliceGroup.slices[0]]); + let result = new tr.model.EventSet; + let filterTask = timeline.addAllEventsMatchingFilterToSelectionAsTask( + new tr.c.TitleOrCategoryFilter('a'), result); + Task.RunSynchronously(filterTask); + assert.isTrue(result.equals(expected)); + + expected = new tr.model.EventSet( + [t1asg.slices[1], t1.sliceGroup.slices[1]]); + result = new tr.model.EventSet(); + filterTask = timeline.addAllEventsMatchingFilterToSelectionAsTask( + new tr.c.TitleOrCategoryFilter('b'), result); + Task.RunSynchronously(filterTask); + assert.isTrue(result.equals(expected)); + }); + + test('emptyThreadsDeleted', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + + const timeline = document.createElement('tr-ui-timeline-track-view'); + timeline.model = model; + + assert.isFalse(timeline.hasVisibleContent); + }); + + test('filteredCounters', function() { + const model = new tr.Model(); + const c1 = model.kernel.getOrCreateCpu(0); + c1.getOrCreateCounter('', 'b'); + + const p1 = model.getOrCreateProcess(1); + const ctr = p1.getOrCreateCounter('', 'a'); + const series = new tr.model.CounterSeries('a', 0); + series.addCounterSample(0, 1); + ctr.addSeries(series); + + const timeline = document.createElement('tr-ui-timeline-track-view'); + timeline.model = model; + + assert.isTrue(timeline.hasVisibleContent); + }); + + test('filteredCpus', function() { + const model = new tr.Model(); + const c1 = model.kernel.getOrCreateCpu(1); + c1.getOrCreateCounter('', 'a'); + + const timeline = document.createElement('tr-ui-timeline-track-view'); + timeline.model = model; + + assert.isTrue(timeline.hasVisibleContent); + }); + + test('filteredProcesses', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + p1.getOrCreateCounter('', 'a'); + + const timeline = document.createElement('tr-ui-timeline-track-view'); + timeline.model = model; + + assert.isTrue(timeline.hasVisibleContent); + }); + + test('filteredThreads', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(2); + t1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({start: 0, duration: 1})); + + const timeline = document.createElement('tr-ui-timeline-track-view'); + timeline.model = model; + + assert.isTrue(timeline.hasVisibleContent); + }); + + test('interestRange', function() { + const events = [ + {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'c', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'c', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'E'}, + {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'}, + {name: 'a', args: {}, pid: 52, ts: 634, cat: 'foo', tid: 53, ph: 'E'} + ]; + const model = tr.c.TestUtils.newModelWithEvents([events]); + const trackView = document.createElement('tr-ui-timeline-track-view'); + trackView.model = model; + this.addHTMLOutput(trackView); + + const slice = model.processes[52].threads[53].sliceGroup.slices[2]; + trackView.viewport.interestRange.setMinAndMax(slice.start, slice.end); + }); + + test('emptyInterestRange', function() { + const events = [ + {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'c', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'c', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'E'}, + {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'}, + {name: 'a', args: {}, pid: 52, ts: 634, cat: 'foo', tid: 53, ph: 'E'} + ]; + const model = tr.c.TestUtils.newModelWithEvents([events]); + const trackView = document.createElement('tr-ui-timeline-track-view'); + trackView.model = model; + this.addHTMLOutput(trackView); + trackView.viewport.interestRange.reset(); + }); + + + test('thinnestInterestRange', function() { + const events = [ + {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'c', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'c', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'E'}, + {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'}, + {name: 'a', args: {}, pid: 52, ts: 634, cat: 'foo', tid: 53, ph: 'E'} + ]; + const model = tr.c.TestUtils.newModelWithEvents([events]); + const trackView = document.createElement('tr-ui-timeline-track-view'); + trackView.model = model; + this.addHTMLOutput(trackView); + trackView.viewport.interestRange.reset(); + + const slice = model.processes[52].threads[53].sliceGroup.slices[2]; + trackView.viewport.interestRange.setMinAndMax(slice.start, slice.start); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view.html new file mode 100644 index 00000000000..9a7a83f4a7b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view.html @@ -0,0 +1,641 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/scripting_controller.html"> +<link rel="import" href="/tracing/metrics/all_metrics.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_view.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/base/dropdown.html"> +<link rel="import" href="/tracing/ui/base/favicons.html"> +<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> +<link rel="import" href="/tracing/ui/base/info_bar_group.html"> +<link rel="import" href="/tracing/ui/base/overlay.html"> +<link rel="import" href="/tracing/ui/base/toolbar_button.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> +<link rel="import" href="/tracing/ui/brushing_state_controller.html"> +<link rel="import" href="/tracing/ui/find_control.html"> +<link rel="import" href="/tracing/ui/find_controller.html"> +<link rel="import" href="/tracing/ui/scripting_control.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_container.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> +<link rel="import" href="/tracing/ui/timeline_view_help_overlay.html"> +<link rel="import" href="/tracing/ui/timeline_view_metadata_overlay.html"> +<link rel="import" href="/tracing/value/ui/preferred_display_unit.html"> + +<dom-module id='tr-ui-timeline-view'> + <template> + <style> + :host { + flex-direction: column; + cursor: default; + display: flex; + font-family: sans-serif; + padding: 0; + } + + #control { + background-color: #e6e6e6; + background-image: -webkit-gradient(linear, 0 0, 0 100%, + from(#E5E5E5), to(#D1D1D1)); + flex: 0 0 auto; + overflow-x: auto; + } + + #control::-webkit-scrollbar { height: 0px; } + + #control > #bar { + font-size: 12px; + display: flex; + flex-direction: row; + margin: 1px; + } + + #control > #bar > #title { + display: flex; + align-items: center; + padding-left: 8px; + padding-right: 8px; + flex: 1 1 auto; + } + + #control > #bar > #left_controls, + #control > #bar > #right_controls { + display: flex; + flex-direction: row; + align-items: stretch; + } + + #control > #bar > #left_controls > * { margin-right: 2px; } + #control > #bar > #right_controls > * { margin-left: 2px; } + #control > #collapsing_controls { display: flex; } + + middle-container { + flex: 1 1 auto; + flex-direction: row; + border-bottom: 1px solid #8e8e8e; + display: flex; + min-height: 0; + } + + middle-container ::content track-view-container { + flex: 1 1 auto; + display: flex; + min-height: 0; + min-width: 0; + overflow-x: hidden; + } + + middle-container ::content track-view-container > * { flex: 1 1 auto; } + middle-container > x-timeline-view-side-panel-container { flex: 0 0 auto; } + tr-ui-b-drag-handle { flex: 0 0 auto; } + tr-ui-a-analysis-view { flex: 0 0 auto; } + + #view_options_dropdown, #process_filter_dropdown { + --dropdown-button: { + -webkit-appearance: none; + align-items: normal; + background-color: rgb(248, 248, 248); + border: 1px solid rgba(0, 0, 0, 0.5); + box-sizing: content-box; + color: rgba(0, 0, 0, 0.8); + font-family: sans-serif; + font-size: 12px; + padding: 2px 5px; + } + } + </style> + + <tv-ui-b-hotkey-controller id="hkc"></tv-ui-b-hotkey-controller> + <div id="control"> + <div id="bar"> + <div id="left_controls"></div> + <div id="title">^_^</div> + <div id="right_controls"> + <tr-ui-b-dropdown id="process_filter_dropdown" label="Processes"></tr-ui-b-dropdown> + <tr-ui-b-toolbar-button id="view_metadata_button"> + M + </tr-ui-b-toolbar-button> + <tr-ui-b-dropdown id="view_options_dropdown" label="View Options"></tr-ui-b-dropdown> + <tr-ui-find-control id="view_find_control"></tr-ui-find-control> + <tr-ui-b-toolbar-button id="view_console_button"> + » + </tr-ui-b-toolbar-button> + <tr-ui-b-toolbar-button id="view_help_button"> + ? + </tr-ui-b-toolbar-button> + </div> + </div> + <div id="collapsing_controls"></div> + <tr-ui-b-info-bar-group id="import-warnings"> + </tr-ui-b-info-bar-group> + </div> + <middle-container> + <slot></slot> + + <tr-ui-side-panel-container id="side_panel_container"> + </tr-ui-side-panel-container> + </middle-container> + <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle> + <tr-ui-a-analysis-view id="analysis"></tr-ui-a-analysis-view> + + <tr-v-ui-preferred-display-unit id="display_unit"> + </tr-v-ui-preferred-display-unit> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-timeline-view', + + created() { + this.trackViewContainer_ = undefined; + + this.queuedModel_ = undefined; + + this.builtPromise_ = undefined; + this.doneBuilding_ = undefined; + }, + + attached() { + this.async(function() { + this.trackViewContainer_ = Polymer.dom(this).querySelector( + '#track_view_container'); + if (!this.trackViewContainer_) { + throw new Error('missing trackviewContainer'); + } + + if (this.queuedModel_) this.updateContents_(); + }); + }, + + ready() { + this.tabIndex = 0; // Let the timeline able to receive key events. + + this.titleEl_ = this.$.title; + this.leftControlsEl_ = this.$.left_controls; + this.rightControlsEl_ = this.$.right_controls; + this.collapsingControlsEl_ = this.$.collapsing_controls; + this.sidePanelContainer_ = this.$.side_panel_container; + + this.brushingStateController_ = new tr.c.BrushingStateController(this); + + this.findCtl_ = this.$.view_find_control; + this.findCtl_.controller = new tr.ui.FindController( + this.brushingStateController_); + + this.scriptingCtl_ = document.createElement('tr-ui-scripting-control'); + this.scriptingCtl_.controller = new tr.c.ScriptingController( + this.brushingStateController_); + + this.sidePanelContainer_.brushingStateController = + this.brushingStateController_; + + if (window.tr.metrics && window.tr.metrics.sh && + window.tr.metrics.sh.SystemHealthMetric) { + this.railScoreSpan_ = document.createElement( + 'tr-metrics-ui-sh-system-health-span'); + Polymer.dom(this.rightControls).appendChild(this.railScoreSpan_); + } else { + this.railScoreSpan_ = undefined; + } + + this.processFilter_ = this.$.process_filter_dropdown; + + this.optionsDropdown_ = this.$.view_options_dropdown; + Polymer.dom(this.optionsDropdown_.iconElement).textContent = 'View Options'; + + this.showFlowEvents_ = false; + Polymer.dom(this.optionsDropdown_).appendChild(tr.ui.b.createCheckBox( + this, 'showFlowEvents', + 'tr.ui.TimelineView.showFlowEvents', false, + 'Flow events')); + this.highlightVSync_ = false; + this.highlightVSyncCheckbox_ = tr.ui.b.createCheckBox( + this, 'highlightVSync', + 'tr.ui.TimelineView.highlightVSync', false, + 'Highlight VSync'); + Polymer.dom(this.optionsDropdown_).appendChild( + this.highlightVSyncCheckbox_); + + this.initMetadataButton_(); + this.initConsoleButton_(); + this.initHelpButton_(); + + Polymer.dom(this.collapsingControls).appendChild(this.scriptingCtl_); + + this.dragEl_ = this.$.drag_handle; + + this.analysisEl_ = this.$.analysis; + this.analysisEl_.brushingStateController = this.brushingStateController_; + + this.addEventListener( + 'requestSelectionChange', + function(e) { + const sc = this.brushingStateController_; + sc.changeSelectionFromRequestSelectionChangeEvent(e.selection); + }.bind(this)); + + // Bookkeeping. + this.onViewportChanged_ = this.onViewportChanged_.bind(this); + this.bindKeyListeners_(); + + this.dragEl_.target = this.analysisEl_; + }, + + get globalMode() { + return this.hotkeyController.globalMode; + }, + + set globalMode(globalMode) { + globalMode = !!globalMode; + this.brushingStateController_.historyEnabled = globalMode; + this.hotkeyController.globalMode = globalMode; + }, + + get hotkeyController() { + return this.$.hkc; + }, + + updateDocumentFavicon() { + let hue; + if (!this.model) { + hue = 'blue'; + } else { + hue = this.model.faviconHue; + } + + let faviconData = tr.ui.b.FaviconsByHue[hue]; + if (faviconData === undefined) { + faviconData = tr.ui.b.FaviconsByHue.blue; + } + + // Find link if its there + let link = Polymer.dom(document.head).querySelector( + 'link[rel="shortcut icon"]'); + if (!link) { + link = document.createElement('link'); + link.rel = 'shortcut icon'; + Polymer.dom(document.head).appendChild(link); + } + link.href = faviconData; + }, + + get showFlowEvents() { + return this.showFlowEvents_; + }, + + set showFlowEvents(showFlowEvents) { + this.showFlowEvents_ = showFlowEvents; + if (!this.trackView_) return; + + this.trackView_.viewport.showFlowEvents = showFlowEvents; + }, + + get highlightVSync() { + return this.highlightVSync_; + }, + + set highlightVSync(highlightVSync) { + this.highlightVSync_ = highlightVSync; + if (!this.trackView_) return; + + this.trackView_.viewport.highlightVSync = highlightVSync; + }, + + initHelpButton_() { + const helpButtonEl = this.$.view_help_button; + + const dlg = new tr.ui.b.Overlay(); + dlg.title = 'Chrome Tracing Help'; + dlg.visible = false; + dlg.appendChild( + document.createElement('tr-ui-timeline-view-help-overlay')); + + function onClick(e) { + dlg.visible = !dlg.visible; + // Stop event so it doesn't trigger new click listener on document. + e.stopPropagation(); + } + + helpButtonEl.addEventListener('click', onClick.bind(this)); + }, + + initConsoleButton_() { + const toggleEl = this.$.view_console_button; + + function onClick(e) { + this.scriptingCtl_.toggleVisibility(); + e.stopPropagation(); + return false; + } + toggleEl.addEventListener('click', onClick.bind(this)); + }, + + initMetadataButton_() { + const showEl = this.$.view_metadata_button; + + function onClick(e) { + const dlg = new tr.ui.b.Overlay(); + dlg.title = 'Metadata for trace'; + + const metadataOverlay = document.createElement( + 'tr-ui-timeline-view-metadata-overlay'); + metadataOverlay.metadata = this.model.metadata; + + Polymer.dom(dlg).appendChild(metadataOverlay); + dlg.visible = true; + + e.stopPropagation(); + return false; + } + showEl.addEventListener('click', onClick.bind(this)); + + this.updateMetadataButtonVisibility_(); + }, + + updateMetadataButtonVisibility_() { + const showEl = this.$.view_metadata_button; + showEl.style.display = + (this.model && this.model.metadata.length) ? '' : 'none'; + }, + + updateProcessList_() { + const dropdown = Polymer.dom(this.processFilter_); + while (dropdown.firstChild) { + dropdown.removeChild(dropdown.firstChild); + } + if (!this.model) return; + + const trackView = + this.trackViewContainer_.querySelector('tr-ui-timeline-track-view'); + const processViews = trackView.processViews; + const cboxes = []; + const updateAll = (checked) => { + for (const cbox of cboxes) { + cbox.checked = checked; + } + }; + + dropdown.appendChild(tr.ui.b.createButton('All', () => updateAll(true))); + dropdown.appendChild(tr.ui.b.createButton('None', () => updateAll(false))); + + for (const view of processViews) { + const cbox = tr.ui.b.createCheckBox(undefined, undefined, undefined, + true, view.processBase.userFriendlyName, + () => view.visible = cbox.checked); + cbox.checked = view.visible; + cboxes.push(cbox); + view.addEventListener('visibility', () => cbox.checked = view.visible); + dropdown.appendChild(cbox); + } + }, + + get leftControls() { + return this.leftControlsEl_; + }, + + get rightControls() { + return this.rightControlsEl_; + }, + + get collapsingControls() { + return this.collapsingControlsEl_; + }, + + get viewTitle() { + return Polymer.dom(this.titleEl_).textContent.substring( + Polymer.dom(this.titleEl_).textContent.length - 2); + }, + + set viewTitle(text) { + if (text === undefined) { + Polymer.dom(this.titleEl_).textContent = ''; + this.titleEl_.hidden = true; + return; + } + this.titleEl_.hidden = false; + Polymer.dom(this.titleEl_).textContent = text; + }, + + get model() { + if (this.trackView_) { + return this.trackView_.model; + } + return undefined; + }, + + set model(model) { + this.build(model); + }, + + async build(model) { + this.queuedModel_ = model; + this.builtPromise_ = new Promise((resolve, reject) => { + this.doneBuilding_ = resolve; + }); + if (this.trackViewContainer_) await this.updateContents_(); + }, + + get builtPromise() { + return this.builtPromise_; + }, + + async updateContents_() { + if (this.trackViewContainer_ === undefined) { + throw new Error( + 'timeline-view.updateContents_ requires trackViewContainer_'); + } + + const model = this.queuedModel_; + this.queuedModel_ = undefined; + + const modelInstanceChanged = model !== this.model; + const modelValid = model && !model.bounds.isEmpty; + + const importWarningsEl = Polymer.dom(this.root).querySelector( + '#import-warnings'); + Polymer.dom(importWarningsEl).textContent = ''; + + // Remove old trackView if the model has completely changed. + if (modelInstanceChanged) { + if (this.railScoreSpan_) { + this.railScoreSpan_.model = undefined; + } + Polymer.dom(this.trackViewContainer_).textContent = ''; + if (this.trackView_) { + this.trackView_.viewport.removeEventListener( + 'change', this.onViewportChanged_); + this.trackView_.brushingStateController = undefined; + this.trackView_.detach(); + this.trackView_ = undefined; + } + this.brushingStateController_.modelWillChange(); + } + + // Create new trackView if needed. + if (modelValid && !this.trackView_) { + this.trackView_ = document.createElement('tr-ui-timeline-track-view'); + this.trackView_.timelineView = this; + + this.trackView.brushingStateController = this.brushingStateController_; + + Polymer.dom(this.trackViewContainer_).appendChild(this.trackView_); + this.trackView_.viewport.addEventListener( + 'change', this.onViewportChanged_); + } + + // Set the model. + if (modelValid) { + this.trackView_.model = model; + this.trackView_.viewport.showFlowEvents = this.showFlowEvents; + this.trackView_.viewport.highlightVSync = this.highlightVSync; + if (this.railScoreSpan_) { + this.railScoreSpan_.model = model; + } + + this.$.display_unit.preferredTimeDisplayMode = model.intrinsicTimeUnit; + } + + if (model) { + for (const warning of model.importWarningsThatShouldBeShownToUser) { + importWarningsEl.addMessage( + `Import Warning: ${warning.type}: ${warning.message}`, [{ + buttonText: 'Dismiss', + onClick(event, infobar) { + infobar.visible = false; + } + }]); + } + } + + // Do things that are selection specific + if (modelInstanceChanged) { + this.updateProcessList_(); + this.updateMetadataButtonVisibility_(); + this.brushingStateController_.modelDidChange(); + this.onViewportChanged_(); + } + + this.doneBuilding_(); + }, + + get brushingStateController() { + return this.brushingStateController_; + }, + + get trackView() { + return this.trackView_; + }, + + get settings() { + if (!this.settings_) { + this.settings_ = new tr.b.Settings(); + } + return this.settings_; + }, + + /** + * Deprecated. Kept around because third_party code occasionally calls + * this to set up embedding. + */ + set focusElement(value) { + throw new Error('This is deprecated. Please set globalMode to true.'); + }, + + bindKeyListeners_() { + const hkc = this.hotkeyController; + + // Shortcuts that *can* steal focus from the console and the filter text + // box. + hkc.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keypress', + keyCode: '`'.charCodeAt(0), + useCapture: true, + thisArg: this, + callback(e) { + this.scriptingCtl_.toggleVisibility(); + if (!this.scriptingCtl_.hasFocus) { + this.focus(); + } + e.stopPropagation(); + } + })); + + // Shortcuts that *can* steal focus from the filter text box. + hkc.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keypress', + keyCode: '/'.charCodeAt(0), + useCapture: true, + thisArg: this, + callback(e) { + if (this.scriptingCtl_.hasFocus) return; + + if (this.findCtl_.hasFocus) { + this.focus(); + } else { + this.findCtl_.focus(); + } + e.preventDefault(); + e.stopPropagation(); + } + })); + + // Shortcuts that *can't* steal focus. + hkc.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keypress', + keyCode: '?'.charCodeAt(0), + useCapture: false, + thisArg: this, + callback(e) { + this.$.view_help_button.click(); + e.stopPropagation(); + } + })); + + hkc.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keypress', + keyCode: 'v'.charCodeAt(0), + useCapture: false, + thisArg: this, + callback(e) { + this.toggleHighlightVSync_(); + e.stopPropagation(); + } + })); + }, + + onViewportChanged_(e) { + const spc = this.sidePanelContainer_; + if (!this.trackView_) { + spc.rangeOfInterest.reset(); + return; + } + + const vr = this.trackView_.viewport.interestRange.asRangeObject(); + if (!spc.rangeOfInterest.equals(vr)) { + spc.rangeOfInterest = vr; + } + + if (this.railScoreSpan_ && this.model) { + this.railScoreSpan_.model = this.model; + } + }, + + toggleHighlightVSync_() { + this.highlightVSyncCheckbox_.checked = + !this.highlightVSyncCheckbox_.checked; + }, + + setFindCtlText(string) { + this.findCtl_.setText(string); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay.html new file mode 100644 index 00000000000..48c2e72df76 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay.html @@ -0,0 +1,245 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html"> +<link rel="import" href="/tracing/ui/base/overlay.html"> + +<dom-module id='tr-ui-timeline-view-help-overlay'> + <template> + <style> + :host { + flex: 1 1 auto; + flex-direction: row; + display: flex; + width: 700px; + } + .column { + width: 50%; + } + h2 { + font-size: 1.2em; + margin: 0; + margin-top: 5px; + text-align: center; + } + h3 { + margin: 0; + margin-left: 126px; + margin-top: 10px; + } + .pair { + flex: 1 1 auto; + flex-direction: row; + display: flex; + } + .command { + font-family: monospace; + margin-right: 5px; + text-align: right; + width: 150px; + } + .action { + font-size: 0.9em; + text-align: left; + width: 200px; + } + tr-ui-b-mouse-mode-icon { + border: 1px solid #888; + border-radius: 3px; + box-shadow: inset 0 0 2px rgba(0,0,0,0.3); + display: inline-block; + margin-right: 1px; + position: relative; + top: 4px; + } + .mouse-mode-icon.pan-mode { + background-position: -1px -11px; + } + .mouse-mode-icon.select-mode { + background-position: -1px -41px; + } + .mouse-mode-icon.zoom-mode { + background-position: -1px -71px; + } + .mouse-mode-icon.timing-mode { + background-position: -1px -101px; + } + </style> + <div class="column left"> + <h2>Navigation</h2> + <div class='pair'> + <div class='command'>w/s</div> + <div class='action'>Zoom in/out (+shift: faster)</div> + </div> + + <div class='pair'> + <div class='command'>a/d</div> + <div class='action'>Pan left/right (+shift: faster)</div> + </div> + + <div class='pair'> + <div class='command'>→/shift-TAB</div> + <div class='action'>Select previous event</div> + </div> + + <div class='pair'> + <div class='command'>←/TAB</div> + <div class='action'>Select next event</div> + </div> + + <h2>Mouse Controls</h2> + <div class='pair'> + <div class='command'>click</div> + <div class='action'>Select event</div> + </div> + <div class='pair'> + <div class='command'>alt-mousewheel</div> + <div class='action'>Zoom in/out</div> + </div> + + <h3> + <tr-ui-b-mouse-mode-icon mode-name="SELECTION"></tr-ui-b-mouse-mode-icon> + Select mode + </h3> + <div class='pair'> + <div class='command'>drag</div> + <div class='action'>Box select</div> + </div> + + <div class='pair'> + <div class='command'><span class='mod'></span>-click/drag</div> + <div class='action'>Add events to the current selection</div> + </div> + + <div class='pair'> + <div class='command'>double click</div> + <div class='action'>Select all events with same title</div> + </div> + + <h3> + <tr-ui-b-mouse-mode-icon mode-name="PANSCAN"></tr-ui-b-mouse-mode-icon> + Pan mode + </h3> + <div class='pair'> + <div class='command'>drag</div> + <div class='action'>Pan the view</div> + </div> + + <h3> + <tr-ui-b-mouse-mode-icon mode-name="ZOOM"></tr-ui-b-mouse-mode-icon> + Zoom mode + </h3> + <div class='pair'> + <div class='command'>drag</div> + <div class='action'>Zoom in/out by dragging up/down</div> + </div> + + <h3> + <tr-ui-b-mouse-mode-icon mode-name="TIMING"></tr-ui-b-mouse-mode-icon> + Timing mode + </h3> + <div class='pair'> + <div class='command'>drag</div> + <div class='action'>Create or move markers</div> + </div> + + <div class='pair'> + <div class='command'>double click</div> + <div class='action'>Set marker range to slice</div> + </div> + </div> + + <div class="column right"> + <h2>General</h2> + <div class='pair'> + <div class='command'>1-4</div> + <div class='action'>Switch mouse mode</div> + </div> + + <div class='pair'> + <div class='command'>shift</div> + <div class='action'>Hold for temporary select</div> + </div> + + <div class='pair'> + <div class='command'>space</div> + <div class='action'>Hold for temporary pan</div> + </div> + + <div class='pair'> + <div class='command'>/</div> + <div class='action'>Search</div> + </div> + + <div class='pair'> + <div class='command'>enter</div> + <div class='action'>Step through search results</div> + </div> + + <div class='pair'> + <div class='command'>f</div> + <div class='action'>Zoom into selection</div> + </div> + + <div class='pair'> + <div class='command'>z/0</div> + <div class='action'>Reset zoom and pan</div> + </div> + + <div class='pair'> + <div class='command'>g/G</div> + <div class='action'>Toggle 60hz grid</div> + </div> + + <div class='pair'> + <div class='command'>v</div> + <div class='action'>Highlight VSync</div> + </div> + + <div class='pair'> + <div class='command'>h</div> + <div class='action'>Toggle low/high details</div> + </div> + + <div class='pair'> + <div class='command'>m</div> + <div class='action'>Mark current selection</div> + </div> + + <div class='pair'> + <div class='command'>p</div> + <div class='action'>Select power samples over current selection interval</div> + </div> + + <div class='pair'> + <div class='command'>`</div> + <div class='action'>Show or hide the scripting console</div> + </div> + + <div class='pair'> + <div class='command'>?</div> + <div class='action'>Show help</div> + </div> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-timeline-view-help-overlay', + + ready() { + const mod = tr.isMac ? 'cmd ' : 'ctrl'; + const spans = Polymer.dom(this.root).querySelectorAll( + 'span.mod'); + for (let i = 0; i < spans.length; i++) { + Polymer.dom(spans[i]).textContent = mod; + } + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay_test.html new file mode 100644 index 00000000000..1b705eea28c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay_test.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/timeline_view_help_overlay.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('inactive', function() { + const el = document.createElement('tr-ui-timeline-view-help-overlay'); + this.addHTMLOutput(el); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay.html new file mode 100644 index 00000000000..45f0efa1a2a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html"> +<link rel="import" href="/tracing/ui/base/overlay.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-timeline-view-metadata-overlay'> + <template> + <style> + :host { + width: 700px; + + overflow: auto; + } + </style> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-timeline-view-metadata-overlay', + + created() { + this.metadata_ = undefined; + }, + + ready() { + this.$.table.tableColumns = [ + { + title: 'name', + value: d => d.name, + }, + { + title: 'value', + value: d => { + const gov = document.createElement('tr-ui-a-generic-object-view'); + gov.object = d.value; + return gov; + }, + } + ]; + }, + + get metadata() { + return this.metadata_; + }, + + set metadata(metadata) { + this.metadata_ = metadata; + this.$.table.tableRows = this.metadata_; + this.$.table.rebuild(); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay_test.html new file mode 100644 index 00000000000..82f974ac7e9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay_test.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/ui/timeline_view_metadata_overlay.html"> +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('inactive', function() { + const el = document.createElement('tr-ui-timeline-view-metadata-overlay'); + el.metadata = [ + { + name: 'clientInfo', + value: { + command_line: './out/Release/Chromium.app/Contents/MacOS/Chromium --enable-threaded-compositing --force-compositing-mode --enable-impl-side-painting --enable-skia-benchmarking --allow-webui-compositing --flag-switches-begin --force-compositing-mode --disable-threaded-compositing --flag-switches-end', // @suppress longLineCheck + version: 'Chrome/29.0.1521.0' + } + }, + { + name: 'somethingElse', + value: 'fascinating!' + } + ]; + + this.addHTMLOutput(el); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_test.html new file mode 100644 index 00000000000..7591f82df77 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_test.html @@ -0,0 +1,217 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/task.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/timeline_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Task = tr.b.Task; + + function setupTimeline() { + const container = document.createElement('track-view-container'); + container.id = 'track_view_container'; + + const view = document.createElement('tr-ui-timeline-view'); + Polymer.dom(view).appendChild(container); + view.trackViewContainer_ = container; + return view; + } + + const createFullyPopulatedModel = function(opt_withError, opt_withMetadata) { + const withError = opt_withError !== undefined ? opt_withError : true; + const withMetadata = opt_withMetadata !== undefined ? + opt_withMetadata : true; + + const numTests = 50; + let testIndex = 0; + const startTime = 0; + + const model = new tr.Model(); + const io = new tr.importer.ImportOptions(); + model.importOptions = io; + + const cpu = model.kernel.getOrCreateCpu(0); + cpu.getOrCreateCounter('Category Name', 'Counter Name Here'); + cpu.createSubSlices(); + + for (testIndex = 0; testIndex < numTests; ++testIndex) { + const process = model.getOrCreateProcess(10000 + testIndex); + if (testIndex % 2 === 0) { + const thread = process.getOrCreateThread('Thread Name Here'); + thread.sliceGroup.pushSlice(new tr.model.ThreadSlice( + 'foo', 'a', 0, startTime, {}, 1)); + thread.sliceGroup.pushSlice(new tr.model.ThreadSlice( + 'bar', 'b', 0, startTime + 23, {}, 10)); + } else { + const thread = process.getOrCreateThread('Name'); + thread.sliceGroup.pushSlice(new tr.model.ThreadSlice( + 'foo', 'a', 0, startTime + 4, {}, 11)); + thread.sliceGroup.pushSlice(new tr.model.ThreadSlice( + 'bar', 'b', 0, startTime + 22, {}, 14)); + } + } + const p1000 = model.getOrCreateProcess(1000); + const objects = p1000.objects; + objects.idWasCreated('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 10); + objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 10, + 'snapshot-1'); + objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 25, + 'snapshot-2'); + objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 40, + 'snapshot-3'); + objects.idWasDeleted('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 45); + model.updateCategories_(); + + // Add a known problematic piece of data to test the import errors UI. + model.importWarning({ + type: 'test_error', + message: 'Synthetic Import Error', + showToUser: true, + }); + model.updateBounds(); + + // Add data with metadata information stored + model.metadata.push({name: 'a', value: 'testA'}); + model.metadata.push({name: 'b', value: 'testB'}); + model.metadata.push({name: 'c', value: 'testC'}); + + return model; + }; + + const visibleTracks = function(trackButtons) { + return trackButtons.reduce(function(numVisible, button) { + const style = button.parentElement.style; + const visible = (style.display.indexOf('none') === -1); + return visible ? numVisible + 1 : numVisible; + }, 0); + }; + + const modelsEquivalent = function(lhs, rhs) { + if (lhs.length !== rhs.length) return false; + + return lhs.every(function(lhsItem, index) { + const rhsItem = rhs[index]; + return rhsItem.regexpText === lhsItem.regexpText && + rhsItem.isOn === lhsItem.isOn; + }); + }; + + test('instantiate', async function() { + const model11 = createFullyPopulatedModel(true, true); + + const view = setupTimeline(); + view.style.height = '400px'; + view.style.border = '1px solid black'; + view.model = model11; + + const simpleButton1 = document.createElement('tr-ui-b-toolbar-button'); + Polymer.dom(simpleButton1).textContent = 'M'; + Polymer.dom(view.leftControls).appendChild(simpleButton1); + + const simpleButton2 = document.createElement('tr-ui-b-toolbar-button'); + Polymer.dom(simpleButton2).textContent = 'am button'; + Polymer.dom(view.leftControls).appendChild(simpleButton2); + + this.addHTMLOutput(view); + await view.builtPromise; + }); + + test('changeModelToSomethingDifferent', async function() { + const model00 = createFullyPopulatedModel(false, false); + const model11 = createFullyPopulatedModel(true, true); + + const view = setupTimeline(); + view.style.height = '400px'; + view.model = model00; + view.model = undefined; + view.model = model11; + view.model = model00; + + this.addHTMLOutput(view); + await view.builtPromise; + }); + + test('setModelToSameThingAgain', async function() { + const model = createFullyPopulatedModel(false, false); + + // Create a view with a model. + const view = setupTimeline(); + this.addHTMLOutput(view); + view.style.height = '400px'; + view.model = model; + const sc = view.brushingStateController; + + // Mutate the model and update the view. + const t123 = model.getOrCreateProcess(123).getOrCreateThread(123); + t123.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + {title: 'somethingUnusual', start: 0, duration: 5})); + view.model = model; + + await view.builtPromise; + + // Verify that the new bits of the model show up in the view. + const selection = new tr.model.EventSet(); + const filter = new tr.c.TitleOrCategoryFilter('somethingUnusual'); + const filterTask = sc.addAllEventsMatchingFilterToSelectionAsTask( + filter, selection); + Task.RunSynchronously(filterTask); + assert.strictEqual(selection.length, 1); + }); + + test('setModelBeforeAttached', async function() { + const view = document.createElement('tr-ui-timeline-view'); + view.style.height = '400px'; + view.model = createFullyPopulatedModel(false, false); + + const container = document.createElement('track-view-container'); + container.id = 'track_view_container'; + Polymer.dom(view).appendChild(container); + this.addHTMLOutput(view); + await view.builtPromise; + }); + + test('filterProcessesUI', async function() { + const view = document.createElement('tr-ui-timeline-view'); + view.style.height = '400px'; + view.model = createFullyPopulatedModel(false, false); + + const container = document.createElement('track-view-container'); + container.id = 'track_view_container'; + Polymer.dom(view).appendChild(container); + this.addHTMLOutput(view); + await view.builtPromise; + + const procFilter = Polymer.dom(view.processFilter_); + const checkboxes = procFilter.querySelectorAll('input[type=checkbox]'); + assert.lengthOf(checkboxes, 52); + + const trackView = + view.trackViewContainer_.querySelector('tr-ui-timeline-track-view'); + const countVisibleTracks = () => trackView.processViews.filter( + view => view.visible).length; + assert.strictEqual(checkboxes.length, trackView.processViews.length); + assert.strictEqual(countVisibleTracks(), 52); + assert.isTrue(trackView.processViews[0].visible); + + checkboxes[0].click(); + assert.strictEqual(countVisibleTracks(), 51); + assert.isFalse(trackView.processViews[0].visible); + assert.isTrue(trackView.processViews[1].visible); + + // Hide a track. Validate that the checkbox updated state correctly. + trackView.processViews[1].visible = false; + assert.isFalse(trackView.processViews[1].visible); + assert.isFalse(checkboxes[1].checked); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport.html new file mode 100644 index 00000000000..58dafb55aff --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport.html @@ -0,0 +1,442 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2012 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/event.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/base/animation.html"> +<link rel="import" href="/tracing/ui/base/animation_controller.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/draw_helpers.html"> +<link rel="import" href="/tracing/ui/timeline_display_transform.html"> +<link rel="import" href="/tracing/ui/timeline_interest_range.html"> +<link rel="import" href="/tracing/ui/tracks/container_to_track_map.html"> +<link rel="import" href="/tracing/ui/tracks/event_to_track_map.html"> + +<script> +'use strict'; + +/** + * @fileoverview Code for the viewport. + */ +tr.exportTo('tr.ui', function() { + const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform; + const TimelineInterestRange = tr.ui.TimelineInterestRange; + + const IDEAL_MAJOR_MARK_DISTANCE_PX = 150; + // Keep 5 digits of precision when rounding the major mark distances. + const MAJOR_MARK_ROUNDING_FACTOR = 100000; + + class AnimationControllerProxy { + constructor(target) { + this.target_ = target; + } + + get panX() { + return this.target_.currentDisplayTransform_.panX; + } + + set panX(panX) { + this.target_.currentDisplayTransform_.panX = panX; + } + + get panY() { + return this.target_.currentDisplayTransform_.panY; + } + + set panY(panY) { + this.target_.currentDisplayTransform_.panY = panY; + } + + get scaleX() { + return this.target_.currentDisplayTransform_.scaleX; + } + + set scaleX(scaleX) { + this.target_.currentDisplayTransform_.scaleX = scaleX; + } + + cloneAnimationState() { + return this.target_.currentDisplayTransform_.clone(); + } + + xPanWorldPosToViewPos(xWorld, xView) { + this.target_.currentDisplayTransform_.xPanWorldPosToViewPos( + xWorld, xView, this.target_.modelTrackContainer_.canvas.clientWidth); + } + } + + /** + * The TimelineViewport manages the transform used for navigating + * within the timeline. It is a simple transform: + * x' = (x+pan) * scale + * + * The timeline code tries to avoid directly accessing this transform, + * instead using this class to do conversion between world and viewspace, + * as well as the math for centering the viewport in various interesting + * ways. + * + * @constructor + * @extends {tr.b.EventTarget} + */ + function TimelineViewport(parentEl) { + this.parentEl_ = parentEl; + this.modelTrackContainer_ = undefined; + this.currentDisplayTransform_ = new TimelineDisplayTransform(); + this.initAnimationController_(); + + // Flow events + this.showFlowEvents_ = false; + + // Highlights. + this.highlightVSync_ = false; + + // High details. + this.highDetails_ = false; + + // Grid system. + this.gridTimebase_ = 0; + this.gridStep_ = 1000 / 60; + this.gridEnabled_ = false; + + // Init logic. + this.hasCalledSetupFunction_ = false; + + this.onResize_ = this.onResize_.bind(this); + this.onModelTrackControllerScroll_ = + this.onModelTrackControllerScroll_.bind(this); + + this.timeMode_ = TimelineViewport.TimeMode.TIME_IN_MS; + // Major mark positions are where the gridlines/ruler marks are placed along + // the x-axis. + this.majorMarkWorldPositions_ = []; + this.majorMarkUnit_ = undefined; + this.interestRange_ = new TimelineInterestRange(this); + + this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap(); + this.containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap(); + + this.dispatchChangeEvent = this.dispatchChangeEvent.bind(this); + } + + TimelineViewport.TimeMode = { + TIME_IN_MS: 0, + REVISIONS: 1 + }; + + TimelineViewport.prototype = { + __proto__: tr.b.EventTarget.prototype, + + /** + * @return {boolean} Whether the current timeline is attached to the + * document. + */ + get isAttachedToDocumentOrInTestMode() { + // Allow not providing a parent element, used by tests. + if (this.parentEl_ === undefined) return; + return tr.ui.b.isElementAttachedToDocument(this.parentEl_); + }, + + onResize_() { + this.dispatchChangeEvent(); + }, + + /** + * Fires the change event on this viewport. Used to notify listeners + * to redraw when the underlying model has been mutated. + */ + dispatchChangeEvent() { + tr.b.dispatchSimpleEvent(this, 'change'); + }, + + detach() { + window.removeEventListener('resize', this.dispatchChangeEvent); + }, + + initAnimationController_() { + this.dtAnimationController_ = new tr.ui.b.AnimationController(); + this.dtAnimationController_.addEventListener( + 'didtick', function(e) { + this.onCurentDisplayTransformChange_(e.oldTargetState); + }.bind(this)); + + this.dtAnimationController_.target = new AnimationControllerProxy(this); + }, + + get currentDisplayTransform() { + return this.currentDisplayTransform_; + }, + + setDisplayTransformImmediately(displayTransform) { + this.dtAnimationController_.cancelActiveAnimation(); + + const oldDisplayTransform = + this.dtAnimationController_.target.cloneAnimationState(); + this.currentDisplayTransform_.set(displayTransform); + this.onCurentDisplayTransformChange_(oldDisplayTransform); + }, + + queueDisplayTransformAnimation(animation) { + if (!(animation instanceof tr.ui.b.Animation)) { + throw new Error('animation must be instanceof tr.ui.b.Animation'); + } + this.dtAnimationController_.queueAnimation(animation); + }, + + onCurentDisplayTransformChange_(oldDisplayTransform) { + // Ensure panY stays clamped in the track container's scroll range. + if (this.modelTrackContainer_) { + this.currentDisplayTransform.panY = tr.b.math.clamp( + this.currentDisplayTransform.panY, + 0, + this.modelTrackContainer_.scrollHeight - + this.modelTrackContainer_.clientHeight); + } + + const changed = !this.currentDisplayTransform.equals(oldDisplayTransform); + const yChanged = this.currentDisplayTransform.panY !== + oldDisplayTransform.panY; + if (yChanged) { + this.modelTrackContainer_.scrollTop = this.currentDisplayTransform.panY; + } + if (changed) { + this.dispatchChangeEvent(); + } + }, + + onModelTrackControllerScroll_(e) { + if (this.dtAnimationController_.activeAnimation && + this.dtAnimationController_.activeAnimation.affectsPanY) { + this.dtAnimationController_.cancelActiveAnimation(); + } + const panY = this.modelTrackContainer_.scrollTop; + this.currentDisplayTransform_.panY = panY; + }, + + get modelTrackContainer() { + return this.modelTrackContainer_; + }, + + set modelTrackContainer(m) { + if (this.modelTrackContainer_) { + this.modelTrackContainer_.removeEventListener('scroll', + this.onModelTrackControllerScroll_); + } + + this.modelTrackContainer_ = m; + this.modelTrackContainer_.addEventListener('scroll', + this.onModelTrackControllerScroll_); + }, + + get showFlowEvents() { + return this.showFlowEvents_; + }, + + set showFlowEvents(showFlowEvents) { + this.showFlowEvents_ = showFlowEvents; + this.dispatchChangeEvent(); + }, + + get highlightVSync() { + return this.highlightVSync_; + }, + + set highlightVSync(highlightVSync) { + this.highlightVSync_ = highlightVSync; + this.dispatchChangeEvent(); + }, + + get highDetails() { + return this.highDetails_; + }, + + set highDetails(highDetails) { + this.highDetails_ = highDetails; + this.dispatchChangeEvent(); + }, + + get gridEnabled() { + return this.gridEnabled_; + }, + + set gridEnabled(enabled) { + if (this.gridEnabled_ === enabled) return; + + this.gridEnabled_ = enabled && true; + this.dispatchChangeEvent(); + }, + + get gridTimebase() { + return this.gridTimebase_; + }, + + set gridTimebase(timebase) { + if (this.gridTimebase_ === timebase) return; + + this.gridTimebase_ = timebase; + this.dispatchChangeEvent(); + }, + + get gridStep() { + return this.gridStep_; + }, + + get interestRange() { + return this.interestRange_; + }, + + get majorMarkWorldPositions() { + return this.majorMarkWorldPositions_; + }, + + get majorMarkUnit() { + switch (this.timeMode_) { + case TimelineViewport.TimeMode.TIME_IN_MS: + return tr.b.Unit.byName.timeInMsAutoFormat; + case TimelineViewport.TimeMode.REVISIONS: + return tr.b.Unit.byName.count; + default: + throw new Error( + 'Cannot get Unit for unsupported time mode ' + this.timeMode_); + } + }, + + get timeMode() { + return this.timeMode_; + }, + + set timeMode(mode) { + this.timeMode_ = mode; + this.dispatchChangeEvent(); + }, + + updateMajorMarkData(viewLWorld, viewRWorld) { + const pixelRatio = window.devicePixelRatio || 1; + const dt = this.currentDisplayTransform; + + const idealMajorMarkDistancePix = + IDEAL_MAJOR_MARK_DISTANCE_PX * pixelRatio; + const idealMajorMarkDistanceWorld = + dt.xViewVectorToWorld(idealMajorMarkDistancePix); + + const majorMarkDistanceWorld = tr.b.math.preferredNumberLargerThanMin( + idealMajorMarkDistanceWorld); + + const firstMajorMark = Math.floor( + viewLWorld / majorMarkDistanceWorld) * majorMarkDistanceWorld; + + this.majorMarkWorldPositions_ = []; + for (let curX = firstMajorMark; + curX < viewRWorld; + curX += majorMarkDistanceWorld) { + this.majorMarkWorldPositions_.push( + Math.floor(MAJOR_MARK_ROUNDING_FACTOR * curX) / + MAJOR_MARK_ROUNDING_FACTOR); + } + }, + + drawMajorMarkLines(ctx, viewHeight) { + // Apply subpixel translate to get crisp lines. + // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ + ctx.save(); + ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0); + + ctx.beginPath(); + for (const majorMark of this.majorMarkWorldPositions_) { + const x = this.currentDisplayTransform.xWorldToView(majorMark); + tr.ui.b.drawLine(ctx, x, 0, x, viewHeight); + } + ctx.strokeStyle = '#ddd'; + ctx.stroke(); + + ctx.restore(); + }, + + drawGridLines(ctx, viewLWorld, viewRWorld, viewHeight) { + if (!this.gridEnabled) return; + + const dt = this.currentDisplayTransform; + let x = this.gridTimebase; + + // Apply subpixel translate to get crisp lines. + // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ + ctx.save(); + ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0); + + ctx.beginPath(); + while (x < viewRWorld) { + if (x >= viewLWorld) { + // Do conversion to viewspace here rather than on + // x to avoid precision issues. + const vx = Math.floor(dt.xWorldToView(x)); + tr.ui.b.drawLine(ctx, vx, 0, vx, viewHeight); + } + + x += this.gridStep; + } + ctx.strokeStyle = 'rgba(255, 0, 0, 0.25)'; + ctx.stroke(); + + ctx.restore(); + }, + + /** + * Helper for selection previous or next. + * @param {boolean} offset If positive, select one forward (next). + * Else, select previous. + * + * @return {boolean} true if current selection changed. + */ + getShiftedSelection(selection, offset) { + const newSelection = new tr.model.EventSet(); + for (const event of selection) { + // If this is a flow event, then move to its slice based on the + // offset direction. + if (event instanceof tr.model.FlowEvent) { + if (offset > 0) { + newSelection.push(event.endSlice); + } else if (offset < 0) { + newSelection.push(event.startSlice); + } else { + /* Do nothing. Zero offsets don't do anything. */ + } + continue; + } + + const track = this.trackForEvent(event); + track.addEventNearToProvidedEventToSelection( + event, offset, newSelection); + } + + if (newSelection.length === 0) return undefined; + + return newSelection; + }, + + rebuildEventToTrackMap() { + // TODO(charliea): Make the event to track map have a similar interface + // to the container to track map so that we can just clear() here. + this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap(); + this.modelTrackContainer_.addEventsToTrackMap(this.eventToTrackMap_); + }, + + rebuildContainerToTrackMap() { + this.containerToTrackMap.clear(); + this.modelTrackContainer_.addContainersToTrackMap( + this.containerToTrackMap); + }, + + trackForEvent(event) { + return this.eventToTrackMap_[event.guid]; + } + }; + + return { + TimelineViewport, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport_test.html new file mode 100644 index 00000000000..5715182e720 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport_test.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/location.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/base/constants.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> +<link rel="import" href="/tracing/ui/tracks/slice_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Location = tr.model.Location; + const Model = tr.Model; + + test('memoization', function() { + const vp = new tr.ui.TimelineViewport(document.createElement('div')); + + const slice = { guid: 1 }; + + vp.modelTrackContainer = { + addEventsToTrackMap(eventToTrackMap) { + eventToTrackMap.addEvent(slice, 'track'); + }, + addEventListener() {} + }; + + assert.isUndefined(vp.trackForEvent(slice)); + vp.rebuildEventToTrackMap(); + + assert.strictEqual(vp.trackForEvent(slice), 'track'); + }); + + test('shiftedSelection', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 5, {}, 1)); + + const viewport = new tr.ui.TimelineViewport(); + const track = new tr.ui.tracks.SliceTrack(viewport); + viewport.modelTrackContainer = track; + track.slices = t1.sliceGroup.slices; + + viewport.rebuildEventToTrackMap(); + + const sel = new tr.model.EventSet(); + sel.push(t1.sliceGroup.slices[0]); + + const shifted = track.viewport.getShiftedSelection(sel, 1); + assert.isTrue(shifted.equals( + new tr.model.EventSet(t1.sliceGroup.slices[1]))); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html new file mode 100644 index 00000000000..571b0543bb9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html"> +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays an array of alert objects. + * @constructor + * @extends {LetterDotTrack} + */ + const AlertTrack = tr.ui.b.define( + 'alert-track', tr.ui.tracks.LetterDotTrack); + + AlertTrack.prototype = { + __proto__: tr.ui.tracks.LetterDotTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.LetterDotTrack.prototype.decorate.call(this, viewport); + this.heading = 'Alerts'; + this.alerts_ = undefined; + }, + + get alerts() { + return this.alerts_; + }, + + set alerts(alerts) { + this.alerts_ = alerts; + if (alerts === undefined) { + this.items = undefined; + return; + } + this.items = this.alerts_.map(function(alert) { + return new tr.ui.tracks.LetterDot( + alert, String.fromCharCode(9888), alert.colorId, alert.start); + }); + } + }; + + return { + AlertTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html new file mode 100644 index 00000000000..4e60180b00e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/global_memory_dump.html"> +<link rel="import" href="/tracing/model/selection_state.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/alert_track.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const AlertTrack = tr.ui.tracks.AlertTrack; + const SelectionState = tr.model.SelectionState; + const Viewport = tr.ui.TimelineViewport; + + const ALERT_INFO_1 = new tr.model.EventInfo( + 'Alert 1', 'One alert'); + const ALERT_INFO_2 = new tr.model.EventInfo( + 'Alert 2', 'Another alert'); + + const createAlerts = function() { + const alerts = [ + new tr.model.Alert(ALERT_INFO_1, 5), + new tr.model.Alert(ALERT_INFO_1, 20), + new tr.model.Alert(ALERT_INFO_2, 35), + new tr.model.Alert(ALERT_INFO_2, 50) + ]; + return alerts; + }; + + test('instantiate', function() { + const alerts = createAlerts(); + alerts[1].selectionState = SelectionState.SELECTED; + + const div = document.createElement('div'); + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = AlertTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + track.alerts = alerts; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 50, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + + + assert.strictEqual(5, track.items[0].start); + }); + + test('modelMapping', function() { + const alerts = createAlerts(); + + const div = document.createElement('div'); + const viewport = new Viewport(div); + const track = AlertTrack(viewport); + track.alerts = alerts; + + const a0 = track.items[0].modelItem; + assert.strictEqual(a0, alerts[0]); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html new file mode 100644 index 00000000000..d922030ce70 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html @@ -0,0 +1,179 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/multi_row_track.html"> +<link rel="import" href="/tracing/ui/tracks/slice_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays a AsyncSliceGroup. + * @constructor + * @extends {MultiRowTrack} + */ + const AsyncSliceGroupTrack = tr.ui.b.define( + 'async-slice-group-track', + tr.ui.tracks.MultiRowTrack); + + AsyncSliceGroupTrack.prototype = { + + __proto__: tr.ui.tracks.MultiRowTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('async-slice-group-track'); + this.group_ = undefined; + }, + + addSubTrack_(slices) { + const track = new tr.ui.tracks.SliceTrack(this.viewport); + track.slices = slices; + Polymer.dom(this).appendChild(track); + track.asyncStyle = true; + return track; + }, + + get group() { + return this.group_; + }, + + set group(group) { + this.group_ = group; + this.buildAndSetSubRows_(); + }, + + get eventContainer() { + return this.group; + }, + + addContainersToTrackMap(containerToTrackMap) { + tr.ui.tracks.MultiRowTrack.prototype.addContainersToTrackMap.apply( + this, arguments); + containerToTrackMap.addContainer(this.group, this); + }, + + buildAndSetSubRows_() { + if (this.group_.viewSubGroups.length <= 1) { + // No nested groups or just only one, the most common case. + const rows = groupAsyncSlicesIntoSubRows(this.group_.slices); + const rowsWithHeadings = rows.map(row => { + return {row, heading: undefined}; + }); + this.setPrebuiltSubRows(this.group_, rowsWithHeadings); + return; + } + + // We have nested grouping level (no further levels supported), + // so process sub-groups separately and preserve their titles. + const rowsWithHeadings = []; + for (const subGroup of this.group_.viewSubGroups) { + const subGroupRows = groupAsyncSlicesIntoSubRows(subGroup.slices); + if (subGroupRows.length === 0) { + continue; + } + for (let i = 0; i < subGroupRows.length; i++) { + rowsWithHeadings.push({ + row: subGroupRows[i], + heading: (i === 0 ? subGroup.title : '') + }); + } + } + this.setPrebuiltSubRows(this.group_, rowsWithHeadings); + } + }; + + /** + * Strip away wrapper slice which are used to group slices into + * a single track but provide no information themselves. + */ + function stripSlice_(slice) { + if (slice.subSlices !== undefined && slice.subSlices.length === 1) { + const subSlice = slice.subSlices[0]; + if (tr.b.math.approximately(subSlice.start, slice.start, 1) && + tr.b.math.approximately(subSlice.duration, slice.duration, 1)) { + return subSlice; + } + } + return slice; + } + + /** + * Unwrap the list of non-overlapping slices into a number of rows where + * the top row holds original slices and additional rows hold nested slices + * of ones from the row above them. + */ + function makeLevelSubRows_(slices) { + const rows = []; + const putSlice = (slice, level) => { + while (rows.length <= level) { + rows.push([]); + } + rows[level].push(slice); + }; + const putSliceRecursively = (slice, level) => { + putSlice(slice, level); + if (slice.subSlices !== undefined) { + for (const subSlice of slice.subSlices) { + putSliceRecursively(subSlice, level + 1); + } + } + }; + + for (const slice of slices) { + putSliceRecursively(stripSlice_(slice), 0); + } + return rows; + } + + /** + * Breaks up the list of slices into a number of rows: + * - Which contain non-overlapping slices. + * - If slice has nested slices, they're placed onto the row below. + * Sorting may be skipped if slices are already sorted by start timestamp. + */ + function groupAsyncSlicesIntoSubRows(slices, opt_skipSort) { + if (!opt_skipSort) { + slices.sort((x, y) => x.start - y.start); + } + + // The algorithm is fairly simple: + // - Level is a group of rows, where the top row holds original slices and + // additional rows hold nested slices of ones from the row above them. + // - Make a level by putting sorted slices, skipping if one's overlapping. + // - Repeat and make more levels while we're having residual slices left. + const rows = []; + let slicesLeft = slices; + while (slicesLeft.length !== 0) { + // Make a level. + const fit = []; + const unfit = []; + let levelEndTime = -1; + + for (const slice of slicesLeft) { + if (slice.start >= levelEndTime) { + // Assuming nested slices lie within parent's boundaries. + levelEndTime = slice.end; + fit.push(slice); + } else { + unfit.push(slice); + } + } + rows.push(...makeLevelSubRows_(fit)); + slicesLeft = unfit; + } + return rows; + } + + return { + AsyncSliceGroupTrack, + groupAsyncSlicesIntoSubRows, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html new file mode 100644 index 00000000000..96003e1b5f2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html @@ -0,0 +1,328 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const AsyncSliceGroup = tr.model.AsyncSliceGroup; + const AsyncSliceGroupTrack = tr.ui.tracks.AsyncSliceGroupTrack; + const Process = tr.model.Process; + const ProcessTrack = tr.ui.tracks.ProcessTrack; + const Thread = tr.model.Thread; + const ThreadTrack = tr.ui.tracks.ThreadTrack; + const newAsyncSlice = tr.c.TestUtils.newAsyncSlice; + const newAsyncSliceNamed = tr.c.TestUtils.newAsyncSliceNamed; + const groupAsyncSlicesIntoSubRows = tr.ui.tracks.groupAsyncSlicesIntoSubRows; + + test('filterSubRows', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + const t1 = new Thread(p1, 1); + const g = new AsyncSliceGroup(t1); + g.push(newAsyncSlice(0, 1, t1, t1)); + const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = g; + + assert.strictEqual(track.children.length, 1); + assert.isTrue(track.hasVisibleContent); + }); + + test('groupAsyncSlicesIntoSubRows_empty', function() { + const rows = groupAsyncSlicesIntoSubRows([]); + assert.strictEqual(rows.length, 0); + }); + + test('groupAsyncSlicesIntoSubRows_trivial', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + const t1 = new Thread(p1, 1); + + const s1 = newAsyncSlice(10, 200, t1, t1); + const s2 = newAsyncSlice(300, 30, t1, t1); + + const slices = [s2, s1]; + const rows = groupAsyncSlicesIntoSubRows(slices); + + assert.strictEqual(rows.length, 1); + assert.sameMembers(rows[0], [s1, s2]); + }); + + test('groupAsyncSlicesIntoSubRows_nonTrivial', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + const t1 = new Thread(p1, 1); + + const s1 = newAsyncSlice(10, 200, t1, t1); // Should be stripped. + const s1s1 = newAsyncSlice(10, 200, t1, t1); + s1.subSlices = [s1s1]; + + const s2 = newAsyncSlice(300, 30, t1, t1); + const s2s1 = newAsyncSlice(300, 10, t1, t1); + const s2s2 = newAsyncSlice(310, 20, t1, t1); // Should not be stripped. + const s2s2s1 = newAsyncSlice(310, 20, t1, t1); + s2s2.subSlices = [s2s2s1]; + s2.subSlices = [s2s2, s2s1]; + + const s3 = newAsyncSlice(200, 50, t1, t1); // Overlaps with s1. + const s3s1 = newAsyncSlice(220, 5, t1, t1); + s3.subSlices = [s3s1]; + + const slices = [s2, s3, s1]; + const rows = groupAsyncSlicesIntoSubRows(slices); + + assert.strictEqual(rows.length, 5); + assert.sameMembers(rows[0], [s1s1, s2]); + assert.sameMembers(rows[1], [s2s1, s2s2]); + assert.sameMembers(rows[2], [s2s2s1]); + assert.sameMembers(rows[3], [s3]); + assert.sameMembers(rows[4], [s3s1]); + }); + + test('rebuildSubRows_twoNonOverlappingSlices', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + const t1 = new Thread(p1, 1); + const g = new AsyncSliceGroup(t1); + const s1 = newAsyncSlice(0, 1, t1, t1); + const subs1 = newAsyncSliceNamed('b', 0, 1, t1, t1); + s1.subSlices = [subs1]; + g.push(s1); + g.push(newAsyncSlice(1, 1, t1, t1)); + const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = g; + const subRows = track.subRows; + assert.strictEqual(subRows.length, 1); + assert.strictEqual(subRows[0].length, 2); + assert.sameMembers(g.slices[1].subSlices, []); + }); + + test('rebuildSubRows_twoOverlappingSlices', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + const t1 = new Thread(p1, 1); + const g = new AsyncSliceGroup(t1); + + const s1 = newAsyncSlice(0, 1, t1, t1); + const subs1 = newAsyncSliceNamed('b', 0, 1, t1, t1); + s1.subSlices = [subs1]; + const s2 = newAsyncSlice(0, 1.5, t1, t1); + const subs2 = newAsyncSliceNamed('b', 0, 1, t1, t1); + s2.subSlices = [subs2]; + g.push(s1); + g.push(s2); + + g.updateBounds(); + + const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = g; + + const subRows = track.subRows; + + assert.strictEqual(subRows.length, 2); + assert.strictEqual(subRows[0].length, 1); + assert.strictEqual(subRows[1].length, 1); + assert.strictEqual(subRows[1][0], g.slices[1].subSlices[0]); + }); + + test('rebuildSubRows_threePartlyOverlappingSlices', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + const t1 = new Thread(p1, 1); + const g = new AsyncSliceGroup(t1); + g.push(newAsyncSlice(0, 1, t1, t1)); + g.push(newAsyncSlice(0, 1.5, t1, t1)); + g.push(newAsyncSlice(1, 1.5, t1, t1)); + g.updateBounds(); + const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = g; + const subRows = track.subRows; + + assert.strictEqual(subRows.length, 2); + assert.strictEqual(subRows[0].length, 2); + assert.strictEqual(subRows[0][0], g.slices[0]); + assert.strictEqual(subRows[0][1], g.slices[2]); + assert.strictEqual(subRows[1][0], g.slices[1]); + assert.strictEqual(subRows[1].length, 1); + assert.sameMembers(g.slices[0].subSlices, []); + assert.sameMembers(g.slices[1].subSlices, []); + assert.sameMembers(g.slices[2].subSlices, []); + }); + + test('rebuildSubRows_threeOverlappingSlices', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + const t1 = new Thread(p1, 1); + const g = new AsyncSliceGroup(t1); + + g.push(newAsyncSlice(0, 1, t1, t1)); + g.push(newAsyncSlice(0, 1.5, t1, t1)); + g.push(newAsyncSlice(2, 1, t1, t1)); + g.updateBounds(); + + const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = g; + + const subRows = track.subRows; + assert.strictEqual(subRows.length, 2); + assert.strictEqual(subRows[0].length, 2); + assert.strictEqual(subRows[1].length, 1); + assert.strictEqual(subRows[0][0], g.slices[0]); + assert.strictEqual(subRows[1][0], g.slices[1]); + assert.strictEqual(subRows[0][1], g.slices[2]); + }); + + test('rebuildSubRows_twoViewSubGroups', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + const t1 = new Thread(p1, 1); + const g = new AsyncSliceGroup(t1); + g.push(newAsyncSliceNamed('foo', 0, 1, t1, t1)); + g.push(newAsyncSliceNamed('foo', 2, 1, t1, t1)); + g.push(newAsyncSliceNamed('bar', 1, 2, t1, t1)); + g.push(newAsyncSliceNamed('bar', 3, 2, t1, t1)); + g.updateBounds(); + + const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = g; + track.heading = 'sup'; + + assert.strictEqual(track.subRows.length, 2); + const subTracks = Polymer.dom(track).children; + assert.strictEqual(subTracks.length, 3); + assert.strictEqual(subTracks[0].slices.length, 0); + assert.strictEqual(subTracks[1].slices.length, 2); + assert.strictEqual(subTracks[2].slices.length, 2); + const headings = + [subTracks[0].heading, subTracks[1].heading, subTracks[2].heading]; + assert.sameMembers(headings, ['foo', 'bar', 'sup']); + }); + + // Tests that no slices and their sub slices overlap. + test('rebuildSubRows_NonOverlappingSubSlices', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + const t1 = new Thread(p1, 1); + const g = new AsyncSliceGroup(t1); + + const slice1 = newAsyncSlice(0, 5, t1, t1); + const slice1Child = newAsyncSlice(1, 2, t1, t1); + slice1.subSlices = [slice1Child]; + const slice2 = newAsyncSlice(3, 5, t1, t1); + const slice3 = newAsyncSlice(5, 4, t1, t1); + const slice3Child = newAsyncSlice(6, 2, t1, t1); + slice3.subSlices = [slice3Child]; + g.push(slice1); + g.push(slice2); + g.push(slice3); + g.updateBounds(); + + const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = g; + + const subRows = track.subRows; + // Checks each sub row to see that we don't have any overlapping slices. + for (let i = 0; i < subRows.length; i++) { + const row = subRows[i]; + for (let j = 0; j < row.length; j++) { + for (let k = j + 1; k < row.length; k++) { + assert.isTrue(row[j].end <= row[k].start); + } + } + } + }); + + test('rebuildSubRows_NonOverlappingSubSlicesThreeNestedLevels', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + const t1 = new Thread(p1, 1); + const g = new AsyncSliceGroup(t1); + + const slice1 = newAsyncSlice(0, 4, t1, t1); + const slice1Child = newAsyncSlice(1, 2, t1, t1); + slice1.subSlices = [slice1Child]; + const slice2 = newAsyncSlice(2, 7, t1, t1); + const slice3 = newAsyncSlice(5, 5, t1, t1); + const slice3Child = newAsyncSlice(6, 3, t1, t1); + const slice3Child2 = newAsyncSlice(7, 1, t1, t1); + slice3.subSlices = [slice3Child]; + slice3Child.subSlices = [slice3Child2]; + g.push(slice1); + g.push(slice2); + g.push(slice3); + g.updateBounds(); + + const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = g; + + const subRows = track.subRows; + // Checks each sub row to see that we don't have any overlapping slices. + for (let i = 0; i < subRows.length; i++) { + const row = subRows[i]; + for (let j = 0; j < row.length; j++) { + for (let k = j + 1; k < row.length; k++) { + assert.isTrue(row[j].end <= row[k].start); + } + } + } + }); + + test('asyncSliceGroupContainerMap', function() { + const vp = new tr.ui.TimelineViewport(); + const containerToTrack = vp.containerToTrackMap; + const model = new tr.Model(); + const process = model.getOrCreateProcess(123); + const thread = process.getOrCreateThread(456); + const group = new AsyncSliceGroup(thread); + + const processTrack = new ProcessTrack(vp); + const threadTrack = new ThreadTrack(vp); + const groupTrack = new AsyncSliceGroupTrack(vp); + processTrack.process = process; + threadTrack.thread = thread; + groupTrack.group = group; + Polymer.dom(processTrack).appendChild(threadTrack); + Polymer.dom(threadTrack).appendChild(groupTrack); + + assert.strictEqual(processTrack.eventContainer, process); + assert.strictEqual(threadTrack.eventContainer, thread); + assert.strictEqual(groupTrack.eventContainer, group); + + assert.isUndefined(containerToTrack.getTrackByStableId('123')); + assert.isUndefined(containerToTrack.getTrackByStableId('123.456')); + assert.isUndefined( + containerToTrack.getTrackByStableId('123.456.AsyncSliceGroup')); + + vp.modelTrackContainer = { + addContainersToTrackMap(containerToTrackMap) { + processTrack.addContainersToTrackMap(containerToTrackMap); + }, + addEventListener() {} + }; + vp.rebuildContainerToTrackMap(); + + // Check that all tracks call childs' addContainersToTrackMap() + // by checking the resulting map. + assert.strictEqual( + containerToTrack.getTrackByStableId('123'), processTrack); + assert.strictEqual( + containerToTrack.getTrackByStableId('123.456'), threadTrack); + assert.strictEqual( + containerToTrack.getTrackByStableId('123.456.AsyncSliceGroup'), + groupTrack); + + // Check the track's eventContainer getter. + assert.strictEqual(processTrack.eventContainer, process); + assert.strictEqual(threadTrack.eventContainer, thread); + assert.strictEqual(groupTrack.eventContainer, group); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html new file mode 100644 index 00000000000..1b73f367636 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/proxy_selectable_item.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A point in a chart series with x (timestamp) and y (value) coordinates + * and an associated model item. The point can optionally also have a base + * y coordinate (which for example corresponds to the bottom edge of the + * associated bar in a bar chart). + * + * @constructor + * @extends {ProxySelectableItem} + */ + function ChartPoint(modelItem, x, y, opt_yBase) { + tr.model.ProxySelectableItem.call(this, modelItem); + this.x = x; + this.y = y; + this.dotLetter = undefined; + + // If the base y-coordinate is undefined, the bottom edge of the associated + // bar in a bar chart will start at the outer bottom edge (which is most + // likely slightly below zero). + this.yBase = opt_yBase; + } + + ChartPoint.prototype = { + __proto__: tr.model.ProxySelectableItem.prototype, + }; + + return { + ChartPoint, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html new file mode 100644 index 00000000000..e2d8bc3e11c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/tracks/chart_point.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ChartPoint = tr.ui.tracks.ChartPoint; + + test('checkFields_withoutYBase', function() { + const event = {}; + const point = new ChartPoint(event, 42, -7); + + assert.strictEqual(point.modelItem, event); + assert.strictEqual(point.x, 42); + assert.strictEqual(point.y, -7); + assert.isUndefined(point.yBase); + }); + + test('checkFields_withYBase', function() { + const event = {}; + const point = new ChartPoint(event, 111, 222, 333); + + assert.strictEqual(point.modelItem, event); + assert.strictEqual(point.x, 111); + assert.strictEqual(point.y, 222); + assert.strictEqual(point.yBase, 333); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html new file mode 100644 index 00000000000..45025d13e0d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html @@ -0,0 +1,566 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/model/proxy_selectable_item.html"> +<link rel="import" href="/tracing/model/selection_state.html"> +<link rel="import" href="/tracing/ui/base/event_presenter.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const ColorScheme = tr.b.ColorScheme; + const EventPresenter = tr.ui.b.EventPresenter; + const SelectionState = tr.model.SelectionState; + + /** + * The type of a chart series. + * @enum + */ + const ChartSeriesType = { + LINE: 0, + AREA: 1 + }; + + // The default rendering configuration for ChartSeries. + const DEFAULT_RENDERING_CONFIG = { + // The type of the chart series. + chartType: ChartSeriesType.LINE, + + // The size of a selected point dot in device-independent pixels (circle + // diameter). + selectedPointSize: 4, + + // The size of an unselected point dot in device-independent pixels (square + // width/height). + unselectedPointSize: 3, + + // Whether the selected dots should be solid circles of the line color, or + // filled with the background's selection color. + solidSelectedDots: false, + + // The color of the chart. + colorId: 0, + + // The width of the top line in device-independent pixels. + lineWidth: 1, + + // Minimum distance between points in physical pixels. Points which are + // closer than this distance will be skipped. + skipDistance: 1, + + // Density in points per physical pixel at which unselected point dots + // become transparent. + unselectedPointDensityTransparent: 0.10, + + // Density in points per physical pixel at which unselected point dots + // become fully opaque. + unselectedPointDensityOpaque: 0.05, + + // Opacity of area chart background. + backgroundOpacity: 0.5, + + // Whether to graph steps between points. Set to false for lines instead. + stepGraph: true + }; + + // The virtual width of the last point in a series (whose rectangle has zero + // width) in world timestamps difference for the purposes of selection. + const LAST_POINT_WIDTH = 16; + + // Constants for sizing and font of points with dot letters. + const DOT_LETTER_RADIUS_PX = 7; + const DOT_LETTER_RADIUS_PADDING_PX = 0.5; + const DOT_LETTER_SELECTED_OUTLINE_WIDTH_PX = 3; + const DOT_LETTER_SELECTED_OUTLINE_DETAIL_WIDTH_PX = 1.5; + const DOT_LETTER_UNSELECTED_OUTLINE_WIDTH_PX = 1; + const DOT_LETTER_FONT_WEIGHT = 400; + const DOT_LETTER_FONT_SIZE_PX = 9; + const DOT_LETTER_FONT = 'Arial'; + + /** + * Visual components of a ChartSeries. + * @enum + */ + const ChartSeriesComponent = { + BACKGROUND: 0, + LINE: 1, + DOTS: 2 + }; + + /** + * A series of points corresponding to a single chart on a chart track. + * This class is responsible for drawing the actual chart onto canvas. + * + * @constructor + */ + function ChartSeries(points, seriesYAxis, opt_renderingConfig) { + this.points = points; + this.seriesYAxis = seriesYAxis; + + this.useRenderingConfig_(opt_renderingConfig); + } + + ChartSeries.prototype = { + useRenderingConfig_(opt_renderingConfig) { + const config = opt_renderingConfig || {}; + + // Store all configuration flags as private properties. + for (const [key, defaultValue] of + Object.entries(DEFAULT_RENDERING_CONFIG)) { + let value = config[key]; + if (value === undefined) { + value = defaultValue; + } + this[key + '_'] = value; + } + + // Avoid unnecessary recomputation in getters. + this.topPadding = this.bottomPadding = Math.max( + this.selectedPointSize_, this.unselectedPointSize_) / 2; + }, + + get range() { + const range = new tr.b.math.Range(); + this.points.forEach(function(point) { + range.addValue(point.y); + }, this); + return range; + }, + + draw(ctx, transform, highDetails) { + if (this.points === undefined || this.points.length === 0) { + return; + } + + // Draw the background. + if (this.chartType_ === ChartSeriesType.AREA) { + this.drawComponent_(ctx, transform, ChartSeriesComponent.BACKGROUND, + highDetails); + } + + // Draw the line at the top. + if (this.chartType_ === ChartSeriesType.LINE || highDetails) { + this.drawComponent_(ctx, transform, ChartSeriesComponent.LINE, + highDetails); + } + + // Draw the points. + this.drawComponent_(ctx, transform, ChartSeriesComponent.DOTS, + highDetails); + }, + + drawComponent_(ctx, transform, component, highDetails) { + // We need to consider extra pixels outside the visible area to avoid + // visual glitches due to non-zero width of dots. + let extraPixels = 0; + if (component === ChartSeriesComponent.DOTS) { + extraPixels = Math.max( + this.selectedPointSize_, this.unselectedPointSize_); + } + const pixelRatio = transform.pixelRatio; + const leftViewX = transform.leftViewX - extraPixels * pixelRatio; + const rightViewX = transform.rightViewX + extraPixels * pixelRatio; + const leftTimestamp = transform.leftTimestamp - extraPixels; + const rightTimestamp = transform.rightTimestamp + extraPixels; + + // Find the index of the first and last (partially) visible points. + const firstVisibleIndex = tr.b.findLowIndexInSortedArray( + this.points, + function(point) { return point.x; }, + leftTimestamp); + let lastVisibleIndex = tr.b.findLowIndexInSortedArray( + this.points, + function(point) { return point.x; }, + rightTimestamp); + if (lastVisibleIndex >= this.points.length || + this.points[lastVisibleIndex].x > rightTimestamp) { + lastVisibleIndex--; + } + + // Pre-calculate component style which does not depend on individual + // points: + // * Skip distance between points, + // * Selected (circle) and unselected (square) dot size, + // * Unselected dot opacity, + // * Selected dot edge color and width, and + // * Line component color and width. + const viewSkipDistance = this.skipDistance_ * pixelRatio; + let selectedCircleRadius; + let letterDotRadius; + let squareSize; + let squareHalfSize; + let squareOpacity; + let unselectedSeriesColor; + let currentStateSeriesColor; + + ctx.save(); + ctx.font = + DOT_LETTER_FONT_WEIGHT + ' ' + + Math.floor(DOT_LETTER_FONT_SIZE_PX * pixelRatio) + 'px ' + + DOT_LETTER_FONT; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + + switch (component) { + case ChartSeriesComponent.DOTS: { + // Selected (circle) and unselected (square) dot size. + selectedCircleRadius = + (this.selectedPointSize_ / 2) * pixelRatio; + letterDotRadius = + Math.max(selectedCircleRadius, DOT_LETTER_RADIUS_PX * pixelRatio); + squareSize = this.unselectedPointSize_ * pixelRatio; + squareHalfSize = squareSize / 2; + unselectedSeriesColor = EventPresenter.getCounterSeriesColor( + this.colorId_, SelectionState.NONE); + + // Unselected dot opacity. + if (!highDetails) { + // Unselected dots are not displayed in 'low details' mode. + squareOpacity = 0; + break; + } + const visibleIndexRange = lastVisibleIndex - firstVisibleIndex; + if (visibleIndexRange <= 0) { + // There is at most one visible point. + squareOpacity = 1; + break; + } + const visibleViewXRange = + transform.worldXToViewX(this.points[lastVisibleIndex].x) - + transform.worldXToViewX(this.points[firstVisibleIndex].x); + if (visibleViewXRange === 0) { + // Multiple visible points which all have the same timestamp. + squareOpacity = 1; + break; + } + const density = visibleIndexRange / visibleViewXRange; + const clampedDensity = tr.b.math.clamp(density, + this.unselectedPointDensityOpaque_, + this.unselectedPointDensityTransparent_); + const densityRange = this.unselectedPointDensityTransparent_ - + this.unselectedPointDensityOpaque_; + squareOpacity = + (this.unselectedPointDensityTransparent_ - clampedDensity) / + densityRange; + break; + } + + case ChartSeriesComponent.LINE: + // Line component color and width. + ctx.strokeStyle = EventPresenter.getCounterSeriesColor( + this.colorId_, SelectionState.NONE); + ctx.lineWidth = this.lineWidth_ * pixelRatio; + break; + + case ChartSeriesComponent.BACKGROUND: + // Style depends on the selection state of individual points. + break; + + default: + throw new Error('Invalid component: ' + component); + } + + // The main loop which draws the given component of visible points from + // left to right. Given the potentially large number of points to draw, + // it should be considered performance-critical and function calls should + // be avoided when possible. + // + // Note that the background and line components are drawn in a delayed + // fashion: the rectangle/line that we draw in an iteration corresponds + // to the *previous* point. This does not apply to the dots, whose + // position is independent of the surrounding dots. + let previousViewX = undefined; + let previousViewY = undefined; + let previousViewYBase = undefined; + let lastSelectionState = undefined; + let baseSteps = undefined; + const startIndex = Math.max(firstVisibleIndex - 1, 0); + let currentViewX; + + for (let i = startIndex; i < this.points.length; i++) { + const currentPoint = this.points[i]; + currentViewX = transform.worldXToViewX(currentPoint.x); + + // Stop drawing the points once we are to the right of the visible area. + if (currentViewX > rightViewX) { + if (previousViewX !== undefined) { + previousViewX = currentViewX = rightViewX; + if (component === ChartSeriesComponent.BACKGROUND || + component === ChartSeriesComponent.LINE) { + ctx.lineTo(currentViewX, previousViewY); + } + } + break; + } + + if (i + 1 < this.points.length) { + const nextPoint = this.points[i + 1]; + const nextViewX = transform.worldXToViewX(nextPoint.x); + + // Skip points that are too close to each other. + if (previousViewX !== undefined && + nextViewX - previousViewX <= viewSkipDistance && + nextViewX < rightViewX) { + continue; + } + + // Start drawing right at the left side of the visible are (instead + // of potentially very far to the left). + if (currentViewX < leftViewX) { + currentViewX = leftViewX; + } + } + + if (previousViewX !== undefined && + currentViewX - previousViewX < viewSkipDistance) { + // We know that nextViewX > previousViewX + viewSkipDistance, so we + // can safely move this points's x over that much without passing + // nextViewX. This ensures that the previous point is visible when + // zoomed out very far. + currentViewX = previousViewX + viewSkipDistance; + } + + const currentViewY = Math.round(transform.worldYToViewY( + currentPoint.y)); + let currentViewYBase; + if (currentPoint.yBase === undefined) { + currentViewYBase = transform.outerBottomViewY; + } else { + currentViewYBase = Math.round( + transform.worldYToViewY(currentPoint.yBase)); + } + const currentSelectionState = currentPoint.selectionState; + if (currentSelectionState !== lastSelectionState) { + const opacity = currentSelectionState === SelectionState.SELECTED ? + 1 : squareOpacity; + currentStateSeriesColor = EventPresenter.getCounterSeriesColor( + this.colorId_, currentSelectionState, opacity); + } + + // Actually draw the given component of the point. + switch (component) { + case ChartSeriesComponent.DOTS: + // Draw the dot for the current point. + if (currentPoint.dotLetter) { + ctx.fillStyle = unselectedSeriesColor; + ctx.strokeStyle = + ColorScheme.getColorForReservedNameAsString('black'); + ctx.beginPath(); + ctx.arc(currentViewX, currentViewY, + letterDotRadius + DOT_LETTER_RADIUS_PADDING_PX, 0, + 2 * Math.PI); + ctx.fill(); + if (currentSelectionState === SelectionState.SELECTED) { + ctx.lineWidth = DOT_LETTER_SELECTED_OUTLINE_WIDTH_PX; + ctx.strokeStyle = + ColorScheme.getColorForReservedNameAsString('olive'); + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(currentViewX, currentViewY, letterDotRadius, 0, + 2 * Math.PI); + ctx.lineWidth = DOT_LETTER_SELECTED_OUTLINE_DETAIL_WIDTH_PX; + ctx.strokeStyle = + ColorScheme.getColorForReservedNameAsString('yellow'); + ctx.stroke(); + } else { + ctx.lineWidth = DOT_LETTER_UNSELECTED_OUTLINE_WIDTH_PX; + ctx.strokeStyle = + ColorScheme.getColorForReservedNameAsString('black'); + ctx.stroke(); + } + ctx.fillStyle = + ColorScheme.getColorForReservedNameAsString('white'); + ctx.fillText(currentPoint.dotLetter, currentViewX, currentViewY); + } else { + ctx.strokeStyle = unselectedSeriesColor; + ctx.lineWidth = pixelRatio; + if (currentSelectionState === SelectionState.SELECTED) { + if (this.solidSelectedDots_) { + ctx.fillStyle = ctx.strokeStyle; + } else { + ctx.fillStyle = currentStateSeriesColor; + } + + ctx.beginPath(); + ctx.arc(currentViewX, currentViewY, selectedCircleRadius, 0, + 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + } else if (squareOpacity > 0) { + ctx.fillStyle = currentStateSeriesColor; + ctx.fillRect(currentViewX - squareHalfSize, + currentViewY - squareHalfSize, squareSize, squareSize); + } + } + break; + + case ChartSeriesComponent.LINE: + // Draw the top line for the previous point (if applicable), or + // prepare for drawing the top line of the current point in the next + // iteration. + if (previousViewX === undefined) { + ctx.beginPath(); + ctx.moveTo(currentViewX, currentViewY); + } else if (this.stepGraph_) { + ctx.lineTo(currentViewX, previousViewY); + } + + // Move to the current point coordinate. + ctx.lineTo(currentViewX, currentViewY); + break; + + case ChartSeriesComponent.BACKGROUND: + // Draw the background for the previous point (if applicable). + if (previousViewX !== undefined && this.stepGraph_) { + ctx.lineTo(currentViewX, previousViewY); + } else { + ctx.lineTo(currentViewX, currentViewY); + } + + // Finish the bottom part of the backgound polygon, change + // background color and start a new polygon when the selection state + // changes (and at the beginning). + if (currentSelectionState !== lastSelectionState) { + if (previousViewX !== undefined) { + let previousBaseStepViewX = currentViewX; + for (let j = baseSteps.length - 1; j >= 0; j--) { + const baseStep = baseSteps[j]; + const baseStepViewX = baseStep.viewX; + const baseStepViewY = baseStep.viewY; + ctx.lineTo(previousBaseStepViewX, baseStepViewY); + ctx.lineTo(baseStepViewX, baseStepViewY); + previousBaseStepViewX = baseStepViewX; + } + ctx.closePath(); + ctx.fill(); + } + ctx.beginPath(); + ctx.fillStyle = EventPresenter.getCounterSeriesColor( + this.colorId_, currentSelectionState, + this.backgroundOpacity_); + ctx.moveTo(currentViewX, currentViewYBase); + baseSteps = []; + } + + if (currentViewYBase !== previousViewYBase || + currentSelectionState !== lastSelectionState) { + baseSteps.push({viewX: currentViewX, viewY: currentViewYBase}); + } + + // Move to the current point coordinate. + ctx.lineTo(currentViewX, currentViewY); + break; + + default: + throw new Error('Not reachable'); + } + + previousViewX = currentViewX; + previousViewY = currentViewY; + previousViewYBase = currentViewYBase; + lastSelectionState = currentSelectionState; + } + + // If we still have an open background or top line polygon (which is + // always the case once we have started drawing due to the delayed fashion + // of drawing), we must close it. + if (previousViewX !== undefined) { + switch (component) { + case ChartSeriesComponent.DOTS: + // All dots were drawn in the main loop. + break; + + case ChartSeriesComponent.LINE: + ctx.stroke(); + break; + + case ChartSeriesComponent.BACKGROUND: { + let previousBaseStepViewX = currentViewX; + for (let j = baseSteps.length - 1; j >= 0; j--) { + const baseStep = baseSteps[j]; + const baseStepViewX = baseStep.viewX; + const baseStepViewY = baseStep.viewY; + ctx.lineTo(previousBaseStepViewX, baseStepViewY); + ctx.lineTo(baseStepViewX, baseStepViewY); + previousBaseStepViewX = baseStepViewX; + } + ctx.closePath(); + ctx.fill(); + break; + } + + default: + throw new Error('Not reachable'); + } + } + ctx.restore(); + }, + + addIntersectingEventsInRangeToSelectionInWorldSpace( + loWX, hiWX, viewPixWidthWorld, selection) { + const points = this.points; + + function getPointWidth(point, i) { + if (i === points.length - 1) { + return LAST_POINT_WIDTH * viewPixWidthWorld; + } + const nextPoint = points[i + 1]; + return nextPoint.x - point.x; + } + + function selectPoint(point) { + point.addToSelection(selection); + } + + tr.b.iterateOverIntersectingIntervals( + this.points, + function(point) { return point.x; }, + getPointWidth, + loWX, + hiWX, + selectPoint); + }, + + addEventNearToProvidedEventToSelection(event, offset, selection) { + if (this.points === undefined) return false; + + const index = this.points.findIndex(point => point.modelItem === event); + if (index === -1) return false; + + const newIndex = index + offset; + if (newIndex < 0 || newIndex >= this.points.length) return false; + + this.points[newIndex].addToSelection(selection); + return true; + }, + + addClosestEventToSelection(worldX, worldMaxDist, loY, hiY, + selection) { + if (this.points === undefined) return; + + const item = tr.b.findClosestElementInSortedArray( + this.points, + function(point) { return point.x; }, + worldX, + worldMaxDist); + + if (!item) return; + + item.addToSelection(selection); + } + }; + + return { + ChartSeries, + ChartSeriesType, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html new file mode 100644 index 00000000000..b07e4276e26 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html @@ -0,0 +1,331 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/selection_state.html"> +<link rel="import" href="/tracing/ui/timeline_display_transform.html"> +<link rel="import" href="/tracing/ui/tracks/chart_point.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html"> +<link rel="import" href="/tracing/ui/tracks/chart_transform.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const EventSet = tr.model.EventSet; + const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform; + const Event = tr.model.Event; + const SelectionState = tr.model.SelectionState; + const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis; + const ChartPoint = tr.ui.tracks.ChartPoint; + const ChartSeries = tr.ui.tracks.ChartSeries; + const ChartTransform = tr.ui.tracks.ChartTransform; + const ChartSeriesType = tr.ui.tracks.ChartSeriesType; + + const CANVAS_WIDTH = 800; + const CANVAS_HEIGHT = 80; + + function getSelectionStateForTesting(index) { + index = index % 7; + if (index < 5) { + return SelectionState.getFromBrighteningLevel(index % 4); + } + return SelectionState.getFromDimmingLevel(index % 3); + } + + function buildSeries(renderingConfig) { + const points = []; + for (let i = 0; i < 60; i++) { + const event = new Event(); + event.index = i; + const phase = i * Math.PI / 15; + const value = Math.sin(phase); + const peakIndex = Math.floor((phase + Math.PI / 2) / (2 * Math.PI)); + const base = peakIndex % 2 === 0 ? undefined : -1 + value / 1.5; + const point = new ChartPoint(event, i - 30, value, base); + points.push(point); + } + const seriesYAxis = new ChartSeriesYAxis(-1, 1); + return new ChartSeries(points, seriesYAxis, renderingConfig); + } + + function drawSeriesWithDetails(test, series, highDetails) { + const div = document.createElement('div'); + const canvas = document.createElement('canvas'); + Polymer.dom(div).appendChild(canvas); + + const pixelRatio = window.devicePixelRatio || 1; + + canvas.width = CANVAS_WIDTH * pixelRatio; + canvas.style.width = CANVAS_WIDTH + 'px'; + canvas.height = CANVAS_HEIGHT * pixelRatio; + canvas.style.height = CANVAS_HEIGHT + 'px'; + + const displayTransform = new TimelineDisplayTransform(); + displayTransform.scaleX = CANVAS_WIDTH * pixelRatio / 60; + displayTransform.panX = 30; + + const transform = new ChartTransform( + displayTransform, + series.seriesYAxis, + CANVAS_WIDTH * pixelRatio, + CANVAS_HEIGHT * pixelRatio, + 10 * pixelRatio, + 10 * pixelRatio, + pixelRatio); + + series.draw(canvas.getContext('2d'), transform, highDetails); + + test.addHTMLOutput(div); + } + + function drawSeries(test, series) { + drawSeriesWithDetails(test, series, false); + drawSeriesWithDetails(test, series, true); + series.stepGraph_ = !series.stepGraph_; + drawSeriesWithDetails(test, series, false); + drawSeriesWithDetails(test, series, true); + } + + test('instantiate_defaultConfig', function() { + const series = buildSeries(undefined); + drawSeries(this, series); + }); + + test('instantiate_lineChart', function() { + const series = buildSeries({ + chartType: ChartSeriesType.LINE, + colorId: 4, + unselectedPointSize: 6, + lineWidth: 2, + unselectedPointDensityOpaque: 0.08 + }); + drawSeries(this, series); + }); + + test('instantiate_areaChart', function() { + const series = buildSeries({ + chartType: ChartSeriesType.AREA, + colorId: 2, + backgroundOpacity: 0.2 + }); + drawSeries(this, series); + }); + + test('instantiate_largeSkipDistance', function() { + const series = buildSeries({ + chartType: ChartSeriesType.AREA, + colorId: 1, + skipDistance: 40, + unselectedPointDensityTransparent: 0.07 + }); + drawSeries(this, series); + }); + + test('instantiate_selection', function() { + const series = buildSeries({ + chartType: ChartSeriesType.AREA, + colorId: 10 + }); + series.points.forEach(function(point, index) { + point.modelItem.selectionState = getSelectionStateForTesting(index); + }); + drawSeries(this, series); + }); + + test('instantiate_selectionWithSolidDots', function() { + const series = buildSeries({ + chartType: ChartSeriesType.AREA, + selectedPointSize: 10, + unselectedPointSize: 6, + solidSelectedDots: true, + colorId: 10 + }); + series.points.forEach(function(point, index) { + point.modelItem.selectionState = getSelectionStateForTesting(index); + }); + drawSeries(this, series); + }); + + test('instantiate_selectionWithAllConfigFlags', function() { + const series = buildSeries({ + chartType: ChartSeriesType.AREA, + selectedPointSize: 10, + unselectedPointSize: 6, + colorId: 15, + lineWidth: 2, + skipDistance: 25, + unselectedPointDensityOpaque: 0.07, + unselectedPointDensityTransparent: 0.09, + backgroundOpacity: 0.8 + }); + series.points.forEach(function(point, index) { + point.modelItem.selectionState = getSelectionStateForTesting(index); + }); + drawSeries(this, series); + }); + + test('instantiate_selectionWithDotLetters', function() { + const series = buildSeries({ + chartType: ChartSeriesType.AREA, + selectedPointSize: 10, + unselectedPointSize: 6, + solidSelectedDots: true, + colorId: 10 + }); + series.points.forEach(function(point, index) { + point.modelItem.selectionState = getSelectionStateForTesting(index); + if (index % 10 === 3) { + point.dotLetter = 'P'; + } else if (index % 10 === 7) { + point.dotLetter = '\u26A0'; + } + }); + drawSeries(this, series); + }); + + test('checkRange', function() { + const series = buildSeries(); + const range = series.range; + assert.isFalse(range.isEmpty); + assert.closeTo(range.min, -1, 0.05); + assert.closeTo(range.max, 1, 0.05); + }); + + test('checkaddIntersectingEventsInRangeToSelectionInWorldSpace', function() { + const series = buildSeries(); + + // Too far left. + let sel = new EventSet(); + series.addIntersectingEventsInRangeToSelectionInWorldSpace( + -1000, -30.5, 40, sel); + assert.lengthOf(sel, 0); + + // Select first point. + sel = new EventSet(); + series.addIntersectingEventsInRangeToSelectionInWorldSpace( + -30.5, -29.5, 40, sel); + assert.strictEqual(tr.b.getOnlyElement(sel).index, 0); + + // Select second point. + sel = new EventSet(); + series.addIntersectingEventsInRangeToSelectionInWorldSpace( + -28.8, -28.2, 40, sel); + assert.strictEqual(tr.b.getOnlyElement(sel).index, 1); + + // Select points in the middle. + sel = new EventSet(); + series.addIntersectingEventsInRangeToSelectionInWorldSpace( + -0.99, 1.01, 40, sel); + assert.lengthOf(sel, 3); + const iterator = sel[Symbol.iterator](); + assert.strictEqual(iterator.next().value.index, 29); + assert.strictEqual(iterator.next().value.index, 30); + assert.strictEqual(iterator.next().value.index, 31); + + // Select the last point. + sel = new EventSet(); + series.addIntersectingEventsInRangeToSelectionInWorldSpace( + 668.99, 668.99, 40, sel); + assert.strictEqual(tr.b.getOnlyElement(sel).index, 59); + + // Too far right. + sel = new EventSet(); + series.addIntersectingEventsInRangeToSelectionInWorldSpace( + 669.01, 2000, 40, sel); + assert.lengthOf(sel, 0); + + // Select everything. + sel = new EventSet(); + series.addIntersectingEventsInRangeToSelectionInWorldSpace( + -29.01, 669.01, 40, sel); + assert.lengthOf(sel, 60); + }); + + test('checkaddEventNearToProvidedEventToSelection', function() { + const series = buildSeries(); + + // Invalid event. + let sel = new EventSet(); + assert.isFalse(series.addEventNearToProvidedEventToSelection( + new Event(), 1, sel)); + assert.lengthOf(sel, 0); + + sel = new EventSet(); + assert.isFalse(series.addEventNearToProvidedEventToSelection( + new Event(), -1, sel)); + assert.lengthOf(sel, 0); + + // First point. + sel = new EventSet(); + assert.isTrue(series.addEventNearToProvidedEventToSelection( + series.points[0].modelItem, 1, sel)); + assert.strictEqual(tr.b.getOnlyElement(sel).index, 1); + + sel = new EventSet(); + assert.isFalse(series.addEventNearToProvidedEventToSelection( + series.points[0].modelItem, -1, sel)); + assert.lengthOf(sel, 0); + + // Middle point. + sel = new EventSet(); + assert.isTrue(series.addEventNearToProvidedEventToSelection( + series.points[30].modelItem, 1, sel)); + assert.strictEqual(tr.b.getOnlyElement(sel).index, 31); + + sel = new EventSet(); + assert.isTrue(series.addEventNearToProvidedEventToSelection( + series.points[30].modelItem, -1, sel)); + assert.strictEqual(tr.b.getOnlyElement(sel).index, 29); + + // Last point. + sel = new EventSet(); + assert.isFalse(series.addEventNearToProvidedEventToSelection( + series.points[59].modelItem, 1, sel)); + assert.lengthOf(sel, 0); + + sel = new EventSet(); + assert.isTrue(series.addEventNearToProvidedEventToSelection( + series.points[59].modelItem, -1, sel)); + assert.strictEqual(tr.b.getOnlyElement(sel).index, 58); + }); + + test('checkAddClosestEventToSelection', function() { + const series = buildSeries(); + + // Left of first point. + let sel = new EventSet(); + series.addClosestEventToSelection(-40, 9, -0.5, 0.5, sel); + assert.lengthOf(sel, 0); + + sel = new EventSet(); + series.addClosestEventToSelection(-40, 11, -0.5, 0.5, sel); + assert.strictEqual(tr.b.getOnlyElement(sel).index, 0); + + // Between two points. + sel = new EventSet(); + series.addClosestEventToSelection(0.4, 0.3, -0.5, 0.5, sel); + assert.lengthOf(sel, 0); + + sel = new EventSet(); + series.addClosestEventToSelection(0.4, 0.4, -0.5, 0.5, sel); + assert.strictEqual(tr.b.getOnlyElement(sel).index, 30); + + // Right of last point. + sel = new EventSet(); + series.addClosestEventToSelection(40, 10, -0.5, 0.5, sel); + assert.lengthOf(sel, 0); + + sel = new EventSet(); + series.addClosestEventToSelection(40, 12, -0.5, 0.5, sel); + assert.strictEqual(tr.b.getOnlyElement(sel).index, 59); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html new file mode 100644 index 00000000000..f34b4c68579 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html @@ -0,0 +1,213 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/unit.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const ColorScheme = tr.b.ColorScheme; + const IDEAL_MAJOR_MARK_HEIGHT_PX = 30; + const AXIS_LABLE_MARGIN_PX = 10; + const AXIS_LABLE_FONT_SIZE_PX = 9; + const AXIS_LABLE_FONT = 'Arial'; + + /** + * A vertical axis for a (set of) chart series which maps an arbitrary range + * of values [min, max] to the unit range [0, 1]. + * + * @constructor + */ + function ChartSeriesYAxis(opt_min, opt_max) { + this.guid_ = tr.b.GUID.allocateSimple(); + this.bounds = new tr.b.math.Range(); + if (opt_min !== undefined) this.bounds.addValue(opt_min); + if (opt_max !== undefined) this.bounds.addValue(opt_max); + } + + ChartSeriesYAxis.prototype = { + get guid() { + return this.guid_; + }, + + valueToUnitRange(value) { + if (this.bounds.isEmpty) { + throw new Error('Chart series y-axis bounds are empty'); + } + const bounds = this.bounds; + if (bounds.range === 0) return 0; + return (value - bounds.min) / bounds.range; + }, + + unitRangeToValue(unitRange) { + if (this.bounds.isEmpty) { + throw new Error('Chart series y-axis bounds are empty'); + } + return unitRange * this.bounds.range + this.bounds.min; + }, + + /** + * Automatically set the y-axis bounds from the range of values of all + * series in a list. + * + * See the description of autoSetFromRange for the optional configuration + * argument flags. + */ + autoSetFromSeries(series, opt_config) { + const range = new tr.b.math.Range(); + series.forEach(function(s) { + range.addRange(s.range); + }, this); + this.autoSetFromRange(range, opt_config); + }, + + /** + * Automatically set the y-axis bound from a range of values. + * + * The following four flags, which affect the behavior of this method with + * respect to already defined bounds, can be present in the optional + * configuration (a flag is assumed to be false if it is not provided or if + * the configuration is not provided): + * + * - expandMin: allow decreasing the min bound (if range.min < this.min) + * - shrinkMin: allow increasing the min bound (if range.min > this.min) + * - expandMax: allow increasing the max bound (if range.max > this.max) + * - shrinkMax: allow decreasing the max bound (if range.max < this.max) + * + * This method will ensure that the resulting bounds are defined and valid + * (i.e. min <= max) provided that they were valid or empty before and the + * value range is non-empty and valid. + * + * Note that unless expanding/shrinking a bound is explicitly enabled in + * the configuration, non-empty bounds will not be changed under any + * circumstances. + * + * Observe that if no configuration is provided (or all flags are set to + * false), this method will only modify the y-axis bounds if they are empty. + */ + autoSetFromRange(range, opt_config) { + if (range.isEmpty) return; + + const bounds = this.bounds; + if (bounds.isEmpty) { + bounds.addRange(range); + return; + } + + if (!opt_config) return; + + const useRangeMin = (opt_config.expandMin && range.min < bounds.min || + opt_config.shrinkMin && range.min > bounds.min); + const useRangeMax = (opt_config.expandMax && range.max > bounds.max || + opt_config.shrinkMax && range.max < bounds.max); + + // Neither bound is modified. + if (!useRangeMin && !useRangeMax) return; + + // Both bounds are modified. Assuming the range argument is a valid + // range, no extra checks are necessary. + if (useRangeMin && useRangeMax) { + bounds.min = range.min; + bounds.max = range.max; + return; + } + + // Only one bound is modified. We must ensure that it doesn't go + // over/under the other (unmodified) bound. + if (useRangeMin) { + bounds.min = Math.min(range.min, bounds.max); + } else { + bounds.max = Math.max(range.max, bounds.min); + } + }, + + + majorMarkHeightWorld_(transform, pixelRatio) { + const idealMajorMarkHeightPx = IDEAL_MAJOR_MARK_HEIGHT_PX * pixelRatio; + const idealMajorMarkHeightWorld = + transform.vectorToWorldDistance(idealMajorMarkHeightPx); + + return tr.b.math.preferredNumberLargerThanMin(idealMajorMarkHeightWorld); + }, + + draw(ctx, transform, showYAxisLabels, showYGridLines) { + if (!showYAxisLabels && !showYGridLines) return; + + const pixelRatio = transform.pixelRatio; + const viewTop = transform.outerTopViewY; + const worldTop = transform.viewYToWorldY(viewTop); + const viewBottom = transform.outerBottomViewY; + const viewHeight = viewBottom - viewTop; + const viewLeft = transform.leftViewX; + const viewRight = transform.rightViewX; + const labelLeft = transform.leftYLabel; + + ctx.save(); + ctx.lineWidth = pixelRatio; + ctx.fillStyle = ColorScheme.getColorForReservedNameAsString('black'); + ctx.textAlign = 'left'; + ctx.textBaseline = 'center'; + + ctx.font = + (AXIS_LABLE_FONT_SIZE_PX * pixelRatio) + 'px ' + AXIS_LABLE_FONT; + + // Draw left edge of chart series. + ctx.beginPath(); + ctx.strokeStyle = ColorScheme.getColorForReservedNameAsString('black'); + tr.ui.b.drawLine( + ctx, viewLeft, viewTop, viewLeft, viewBottom, viewLeft); + ctx.stroke(); + ctx.closePath(); + + // Draw y-axis ticks and gridlines. + ctx.beginPath(); + ctx.strokeStyle = ColorScheme.getColorForReservedNameAsString('grey'); + + const majorMarkHeight = this.majorMarkHeightWorld_(transform, pixelRatio); + const maxMajorMark = Math.max(transform.viewYToWorldY(viewTop), + Math.abs(transform.viewYToWorldY(viewBottom))); + for (let curWorldY = 0; + curWorldY <= maxMajorMark; + curWorldY += majorMarkHeight) { + const roundedUnitValue = Math.floor(curWorldY * 1000000) / 1000000; + const curViewYPositive = transform.worldYToViewY(curWorldY); + if (curViewYPositive >= viewTop) { + if (showYAxisLabels) { + ctx.fillText(roundedUnitValue, viewLeft + AXIS_LABLE_MARGIN_PX, + curViewYPositive - AXIS_LABLE_MARGIN_PX); + } + if (showYGridLines) { + tr.ui.b.drawLine( + ctx, viewLeft, curViewYPositive, viewRight, curViewYPositive); + } + } + + const curViewYNegative = transform.worldYToViewY(-1 * curWorldY); + if (curViewYNegative <= viewBottom) { + if (showYAxisLabels) { + ctx.fillText(roundedUnitValue, viewLeft + AXIS_LABLE_MARGIN_PX, + curViewYNegative - AXIS_LABLE_MARGIN_PX); + } + if (showYGridLines) { + tr.ui.b.drawLine( + ctx, viewLeft, curViewYNegative, viewRight, curViewYNegative); + } + } + } + ctx.stroke(); + ctx.restore(); + } + }; + + return { + ChartSeriesYAxis, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html new file mode 100644 index 00000000000..4a759e040d4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html @@ -0,0 +1,313 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/tracks/chart_point.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis; + const ChartPoint = tr.ui.tracks.ChartPoint; + const ChartSeries = tr.ui.tracks.ChartSeries; + const Range = tr.b.math.Range; + + function buildRange() { + const range = new Range(); + for (let i = 0; i < arguments.length; i++) { + range.addValue(arguments[i]); + } + return range; + } + + function buildSeries() { + const points = []; + for (let i = 0; i < arguments.length; i++) { + points.push(new ChartPoint(undefined, i, arguments[i])); + } + return new ChartSeries(points, new ChartSeriesYAxis()); + } + + test('instantiate_emptyBounds', function() { + const seriesYAxis = new ChartSeriesYAxis(); + assert.isTrue(seriesYAxis.bounds.isEmpty); + }); + + test('instantiate_nonEmptyBounds', function() { + const seriesYAxis = new ChartSeriesYAxis(-2, 12); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, -2); + assert.strictEqual(seriesYAxis.bounds.max, 12); + }); + + test('instantiate_equalBounds', function() { + const seriesYAxis = new ChartSeriesYAxis(2.72); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 2.72); + assert.strictEqual(seriesYAxis.bounds.max, 2.72); + }); + + test('checkValueToUnitRange_emptyBounds', function() { + const seriesYAxis = new ChartSeriesYAxis(); + assert.throws(function() { seriesYAxis.valueToUnitRange(42); }); + }); + + test('checkValueToUnitRange_nonEmptyBounds', function() { + const seriesYAxis = new ChartSeriesYAxis(10, 20); + + assert.strictEqual(seriesYAxis.valueToUnitRange(0), -1); + assert.strictEqual(seriesYAxis.valueToUnitRange(10), 0); + assert.strictEqual(seriesYAxis.valueToUnitRange(15), 0.5); + assert.strictEqual(seriesYAxis.valueToUnitRange(20), 1); + assert.strictEqual(seriesYAxis.valueToUnitRange(30), 2); + }); + + test('checkValueToUnitRange_equalBounds', function() { + const seriesYAxis = new ChartSeriesYAxis(3.14); + + assert.strictEqual(seriesYAxis.valueToUnitRange(0), 0); + assert.strictEqual(seriesYAxis.valueToUnitRange(3.14), 0); + assert.strictEqual(seriesYAxis.valueToUnitRange(6.28), 0); + }); + + test('checkAutoSetFromRange_emptyBounds', function() { + // Empty range. + let seriesYAxis = new ChartSeriesYAxis(); + seriesYAxis.autoSetFromRange(buildRange()); + assert.isTrue(seriesYAxis.bounds.isEmpty); + + // Non-empty range. + seriesYAxis = new ChartSeriesYAxis(); + seriesYAxis.autoSetFromRange(buildRange(-1, 3)); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, -1); + assert.strictEqual(seriesYAxis.bounds.max, 3); + }); + + test('checkAutoSetFromRange_nonEmptyBounds', function() { + // Empty range. + let seriesYAxis = new ChartSeriesYAxis(0, 1); + seriesYAxis.autoSetFromRange(buildRange()); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 0); + assert.strictEqual(seriesYAxis.bounds.max, 1); + + // No configuration. + seriesYAxis = new ChartSeriesYAxis(2, 3); + seriesYAxis.autoSetFromRange(buildRange(1, 4)); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 2); + assert.strictEqual(seriesYAxis.bounds.max, 3); + + // Allow expanding min. + seriesYAxis = new ChartSeriesYAxis(-2, -1); + seriesYAxis.autoSetFromRange(buildRange(-3, 0), {expandMin: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, -3); + assert.strictEqual(seriesYAxis.bounds.max, -1); + + // Allow shrinking min. + seriesYAxis = new ChartSeriesYAxis(-2, -1); + seriesYAxis.autoSetFromRange(buildRange(-1.5, 0.5), {shrinkMin: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, -1.5); + assert.strictEqual(seriesYAxis.bounds.max, -1); + + seriesYAxis = new ChartSeriesYAxis(7, 8); + seriesYAxis.autoSetFromRange(buildRange(9, 10), {shrinkMin: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 8); + assert.strictEqual(seriesYAxis.bounds.max, 8); + + // Allow expanding max. + seriesYAxis = new ChartSeriesYAxis(19, 20); + seriesYAxis.autoSetFromRange(buildRange(18, 21), {expandMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 19); + assert.strictEqual(seriesYAxis.bounds.max, 21); + + // Allow shrinking max. + seriesYAxis = new ChartSeriesYAxis(30, 32); + seriesYAxis.autoSetFromRange(buildRange(29, 31), {shrinkMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 30); + assert.strictEqual(seriesYAxis.bounds.max, 31); + + seriesYAxis = new ChartSeriesYAxis(41, 42); + seriesYAxis.autoSetFromRange(buildRange(39, 40), {shrinkMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 41); + assert.strictEqual(seriesYAxis.bounds.max, 41); + + // Allow shrinking both bounds. + seriesYAxis = new ChartSeriesYAxis(50, 53); + seriesYAxis.autoSetFromRange(buildRange(51, 52), + {shrinkMin: true, shrinkMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 51); + assert.strictEqual(seriesYAxis.bounds.max, 52); + + seriesYAxis = new ChartSeriesYAxis(50, 53); + seriesYAxis.autoSetFromRange(buildRange(49, 52), + {shrinkMin: true, shrinkMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 50); + assert.strictEqual(seriesYAxis.bounds.max, 52); + + seriesYAxis = new ChartSeriesYAxis(50, 53); + seriesYAxis.autoSetFromRange(buildRange(51, 54), + {shrinkMin: true, shrinkMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 51); + assert.strictEqual(seriesYAxis.bounds.max, 53); + + seriesYAxis = new ChartSeriesYAxis(50, 53); + seriesYAxis.autoSetFromRange(buildRange(49, 54), + {shrinkMin: true, shrinkMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 50); + assert.strictEqual(seriesYAxis.bounds.max, 53); + + // Allow expanding both bounds. + seriesYAxis = new ChartSeriesYAxis(60, 61); + seriesYAxis.autoSetFromRange(buildRange(0, 100), + {expandMin: true, expandMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 0); + assert.strictEqual(seriesYAxis.bounds.max, 100); + + seriesYAxis = new ChartSeriesYAxis(60, 61); + seriesYAxis.autoSetFromRange(buildRange(60.5, 100), + {expandMin: true, expandMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 60); + assert.strictEqual(seriesYAxis.bounds.max, 100); + + seriesYAxis = new ChartSeriesYAxis(60, 61); + seriesYAxis.autoSetFromRange(buildRange(0, 60.5), + {expandMin: true, expandMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 0); + assert.strictEqual(seriesYAxis.bounds.max, 61); + + seriesYAxis = new ChartSeriesYAxis(60, 61); + seriesYAxis.autoSetFromRange(buildRange(60.2, 60.8), + {expandMin: true, expandMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 60); + assert.strictEqual(seriesYAxis.bounds.max, 61); + + // Allow shrinking min and expanding max. + seriesYAxis = new ChartSeriesYAxis(60, 61); + seriesYAxis.autoSetFromRange(buildRange(62, 63), + {shrinkMin: true, expandMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 62); + assert.strictEqual(seriesYAxis.bounds.max, 63); + + seriesYAxis = new ChartSeriesYAxis(60, 61); + seriesYAxis.autoSetFromRange(buildRange(59, 63), + {shrinkMin: true, expandMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 60); + assert.strictEqual(seriesYAxis.bounds.max, 63); + + seriesYAxis = new ChartSeriesYAxis(60, 61); + seriesYAxis.autoSetFromRange(buildRange(60.2, 60.8), + {shrinkMin: true, expandMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 60.2); + assert.strictEqual(seriesYAxis.bounds.max, 61); + + seriesYAxis = new ChartSeriesYAxis(60, 61); + seriesYAxis.autoSetFromRange(buildRange(59, 60.5), + {shrinkMin: true, expandMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 60); + assert.strictEqual(seriesYAxis.bounds.max, 61); + + // Allow expanding min and shrinking max. + seriesYAxis = new ChartSeriesYAxis(60, 61); + seriesYAxis.autoSetFromRange(buildRange(62, 63), + {expandMin: true, shrinkMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 60); + assert.strictEqual(seriesYAxis.bounds.max, 61); + + seriesYAxis = new ChartSeriesYAxis(60, 61); + seriesYAxis.autoSetFromRange(buildRange(59, 63), + {expandMin: true, shrinkMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 59); + assert.strictEqual(seriesYAxis.bounds.max, 61); + + seriesYAxis = new ChartSeriesYAxis(60, 61); + seriesYAxis.autoSetFromRange(buildRange(60.2, 60.8), + {expandMin: true, shrinkMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 60); + assert.strictEqual(seriesYAxis.bounds.max, 60.8); + + seriesYAxis = new ChartSeriesYAxis(60, 61); + seriesYAxis.autoSetFromRange(buildRange(59, 60.5), + {expandMin: true, shrinkMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 59); + assert.strictEqual(seriesYAxis.bounds.max, 60.5); + + // Allow everything. + seriesYAxis = new ChartSeriesYAxis(200, 250); + seriesYAxis.autoSetFromRange(buildRange(150, 175), + {expandMin: true, expandMax: true, shrinkMin: true, shrinkMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 150); + assert.strictEqual(seriesYAxis.bounds.max, 175); + + seriesYAxis = new ChartSeriesYAxis(0, 0.1); + seriesYAxis.autoSetFromRange(buildRange(0.2, 0.3), + {expandMin: true, expandMax: true, shrinkMin: true, shrinkMax: true}); + assert.isFalse(seriesYAxis.bounds.isEmpty); + assert.strictEqual(seriesYAxis.bounds.min, 0.2); + assert.strictEqual(seriesYAxis.bounds.max, 0.3); + }); + + test('checkAutoSetFromSeries_noSeries', function() { + const seriesYAxis = new ChartSeriesYAxis(-100, 100); + const series = []; + + seriesYAxis.autoSetFromSeries(series); + assert.strictEqual(seriesYAxis.bounds.min, -100); + assert.strictEqual(seriesYAxis.bounds.max, 100); + }); + + test('checkAutoSetFromSeries_oneSeries', function() { + const seriesYAxis = new ChartSeriesYAxis(-100, 100); + const series = [buildSeries(-80, 100, -40, 200)]; + + seriesYAxis.autoSetFromSeries(series, {shrinkMin: true, expandMax: true}); + assert.strictEqual(seriesYAxis.bounds.min, -80); + assert.strictEqual(seriesYAxis.bounds.max, 200); + }); + + test('checkAutoSetFromSeries_multipleSeries', function() { + const seriesYAxis = new ChartSeriesYAxis(-100, 100); + const series = [ + buildSeries(0, 20, 10, 30), + buildSeries(), + buildSeries(-500) + ]; + + seriesYAxis.autoSetFromSeries(series, {expandMin: true, shrinkMax: true}); + assert.strictEqual(seriesYAxis.bounds.min, -500); + assert.strictEqual(seriesYAxis.bounds.max, 30); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html new file mode 100644 index 00000000000..58ef08d651c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html @@ -0,0 +1,281 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/heading.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/chart_transform.html"> +<link rel="import" href="/tracing/ui/tracks/track.html"> + +<style> +.chart-track { + height: 30px; + position: relative; +} +</style> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays a chart. + * + * @constructor + * @extends {Track} + */ + const ChartTrack = + tr.ui.b.define('chart-track', tr.ui.tracks.Track); + + ChartTrack.prototype = { + __proto__: tr.ui.tracks.Track.prototype, + + decorate(viewport) { + tr.ui.tracks.Track.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('chart-track'); + this.series_ = undefined; + this.axes_ = undefined; + + // GUID -> {axis: ChartSeriesYAxis, series: [ChartSeries]}. + this.axisGuidToAxisData_ = undefined; + + // The maximum top and bottom padding of all series. + this.topPadding_ = undefined; + this.bottomPadding_ = undefined; + + this.showYAxisLabels_ = undefined; + this.showGridLines_ = undefined; + + this.heading_ = document.createElement('tr-ui-b-heading'); + Polymer.dom(this).appendChild(this.heading_); + }, + + set heading(heading) { + this.heading_.heading = heading; + }, + + get heading() { + return this.heading_.heading; + }, + + set tooltip(tooltip) { + this.heading_.tooltip = tooltip; + }, + + get series() { + return this.series_; + }, + + /** + * Set the list of chart series to be displayed on this track. The list + * is assumed to be sorted in increasing z-order (i.e. the last series in + * the list will be drawn at the top). + */ + set series(series) { + this.series_ = series; + this.calculateAxisDataAndPadding_(); + this.invalidateDrawingContainer(); + }, + + get height() { + return window.getComputedStyle(this).height; + }, + + set height(height) { + this.style.height = height; + this.invalidateDrawingContainer(); + }, + + get showYAxisLabels() { + return this.showYAxisLabels_; + }, + + set showYAxisLabels(showYAxisLabels) { + this.showYAxisLabels_ = showYAxisLabels; + this.invalidateDrawingContainer(); + }, + + get showGridLines() { + return this.showGridLines_; + }, + + set showGridLines(showGridLines) { + this.showGridLines_ = showGridLines; + this.invalidateDrawingContainer(); + }, + + get hasVisibleContent() { + return !!this.series && this.series.length > 0; + }, + + calculateAxisDataAndPadding_() { + if (!this.series_) { + this.axes_ = undefined; + this.axisGuidToAxisData_ = undefined; + this.topPadding_ = undefined; + this.bottomPadding_ = undefined; + return; + } + + const axisGuidToAxisData = {}; + let topPadding = 0; + let bottomPadding = 0; + + this.series_.forEach(function(series) { + const seriesYAxis = series.seriesYAxis; + const axisGuid = seriesYAxis.guid; + if (!(axisGuid in axisGuidToAxisData)) { + axisGuidToAxisData[axisGuid] = { + axis: seriesYAxis, + series: [] + }; + if (!this.axes_) this.axes_ = []; + this.axes_.push(seriesYAxis); + } + axisGuidToAxisData[axisGuid].series.push(series); + topPadding = Math.max(topPadding, series.topPadding); + bottomPadding = Math.max(bottomPadding, series.bottomPadding); + }, this); + + this.axisGuidToAxisData_ = axisGuidToAxisData; + this.topPadding_ = topPadding; + this.bottomPadding_ = bottomPadding; + }, + + draw(type, viewLWorld, viewRWorld, viewHeight) { + switch (type) { + case tr.ui.tracks.DrawType.GENERAL_EVENT: + this.drawChart_(viewLWorld, viewRWorld); + break; + } + }, + + drawChart_(viewLWorld, viewRWorld) { + if (!this.series_) return; + + const ctx = this.context(); + + // Get track drawing parameters. + const displayTransform = this.viewport.currentDisplayTransform; + const pixelRatio = window.devicePixelRatio || 1; + const bounds = this.getBoundingClientRect(); + const highDetails = this.viewport.highDetails; + + // Pre-multiply all device-independent pixel parameters with the pixel + // ratio to avoid unnecessary recomputation in the performance-critical + // drawing code. + const width = bounds.width * pixelRatio; + const height = bounds.height * pixelRatio; + const topPadding = this.topPadding_ * pixelRatio; + const bottomPadding = this.bottomPadding_ * pixelRatio; + + // Set up clipping. + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, width, height); + ctx.clip(); + + // TODO(aiolos): Add support for secondary y-axis on right side of chart. + // https://github.com/catapult-project/catapult/issues/3008 + // Draw y-axis grid lines. + if (this.axes_) { + if ((this.showGridLines_ || this.showYAxisLabels_) && + this.axes_.length > 1) { + throw new Error('Only one axis allowed when showing grid lines.'); + } + for (const yAxis of this.axes_) { + const chartTransform = new tr.ui.tracks.ChartTransform( + displayTransform, yAxis, width, height, + topPadding, bottomPadding, pixelRatio); + yAxis.draw( + ctx, chartTransform, this.showYAxisLabels_, this.showGridLines_); + } + } + + // Draw all series in the increasing z-order. + for (const series of this.series) { + const chartTransform = new tr.ui.tracks.ChartTransform( + displayTransform, series.seriesYAxis, width, height, topPadding, + bottomPadding, pixelRatio); + series.draw(ctx, chartTransform, highDetails); + } + + // Stop clipping. + ctx.restore(); + }, + + addEventsToTrackMap(eventToTrackMap) { + // TODO(petrcermak): Consider adding the series to the track map instead + // of the track (a potential performance optimization). + this.series_.forEach(function(series) { + series.points.forEach(function(point) { + point.addToTrackMap(eventToTrackMap, this); + }, this); + }, this); + }, + + addIntersectingEventsInRangeToSelectionInWorldSpace( + loWX, hiWX, viewPixWidthWorld, selection) { + this.series_.forEach(function(series) { + series.addIntersectingEventsInRangeToSelectionInWorldSpace( + loWX, hiWX, viewPixWidthWorld, selection); + }, this); + }, + + addEventNearToProvidedEventToSelection(event, offset, selection) { + let foundItem = false; + this.series_.forEach(function(series) { + foundItem = foundItem || series.addEventNearToProvidedEventToSelection( + event, offset, selection); + }, this); + return foundItem; + }, + + addAllEventsMatchingFilterToSelection(filter, selection) { + // Do nothing. + }, + + addClosestEventToSelection(worldX, worldMaxDist, loY, hiY, + selection) { + this.series_.forEach(function(series) { + series.addClosestEventToSelection( + worldX, worldMaxDist, loY, hiY, selection); + }, this); + }, + + /** + * Automatically set the bounds of all axes on this track from the range of + * values of all series (in this track) associated with each of them. + * + * See the description of ChartSeriesYAxis.autoSetFromRange for the optional + * configuration argument flags. + */ + autoSetAllAxes(opt_config) { + for (const axisData of Object.values(this.axisGuidToAxisData_)) { + const seriesYAxis = axisData.axis; + const series = axisData.series; + seriesYAxis.autoSetFromSeries(series, opt_config); + } + }, + + /** + * Automatically set the bounds of the provided axis from the range of + * values of all series (in this track) associated with it. + * + * See the description of ChartSeriesYAxis.autoSetFromRange for the optional + * configuration argument flags. + */ + autoSetAxis(seriesYAxis, opt_config) { + const series = this.axisGuidToAxisData_[seriesYAxis.guid].series; + seriesYAxis.autoSetFromSeries(series, opt_config); + } + }; + + return { + ChartTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html new file mode 100644 index 00000000000..405640a9b2c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html @@ -0,0 +1,454 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/xhr.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/selection_state.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> +<link rel="import" href="/tracing/ui/tracks/chart_point.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html"> +<link rel="import" href="/tracing/ui/tracks/chart_track.html"> +<link rel="import" href="/tracing/ui/tracks/event_to_track_map.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis; + const ChartPoint = tr.ui.tracks.ChartPoint; + const ChartSeries = tr.ui.tracks.ChartSeries; + const ChartSeriesType = tr.ui.tracks.ChartSeriesType; + const ChartTrack = tr.ui.tracks.ChartTrack; + const Event = tr.model.Event; + const EventSet = tr.model.EventSet; + const EventToTrackMap = tr.ui.tracks.EventToTrackMap; + const SelectionState = tr.model.SelectionState; + const Viewport = tr.ui.TimelineViewport; + + function buildPoint(x, y) { + const event = new Event(); + return new ChartPoint(event, x, y); + } + + function buildTrack(opt_args) { + const viewport = (opt_args && opt_args.viewport) ? + opt_args.viewport : new Viewport(document.createElement('div')); + + const seriesYAxis1 = new ChartSeriesYAxis(0, 2.5); + + const points1 = [ + buildPoint(-2.5, 2), + buildPoint(-1.5, 1), + buildPoint(-0.5, 0), + buildPoint(0.5, 1), + buildPoint(1.5, 2), + buildPoint(2.5, 0) + ]; + const renderingConfig1 = { + chartType: ChartSeriesType.AREA, + colorId: 6, + selectedPointSize: 7 + }; + if (opt_args && opt_args.stepGraph !== undefined) { + renderingConfig1.stepGraph = opt_args.stepGraph; + } + const series1 = new ChartSeries(points1, seriesYAxis1, renderingConfig1); + + const points2 = [ + buildPoint(-2.3, 0.2), + buildPoint(-1.3, 1.2), + buildPoint(-0.3, 2.2), + buildPoint(0.3, 1.2), + buildPoint(1.3, 0.2), + buildPoint(2.3, 0) + ]; + const renderingConfig2 = { + chartType: ChartSeriesType.AREA, + colorId: 4, + selectedPointSize: 10 + }; + if (opt_args && opt_args.stepGraph !== undefined) { + renderingConfig2.stepGraph = opt_args.stepGraph; + } + const series2 = new ChartSeries(points2, seriesYAxis1, renderingConfig2); + + const seriesList = [series1, series2]; + + if (!opt_args || !opt_args.singleAxis) { + const seriesYAxis2 = new ChartSeriesYAxis(-100, 100); + const points3 = [ + buildPoint(-3, -50), + buildPoint(-2.4, -40), + buildPoint(-1.8, -30), + buildPoint(-1.2, -20), + buildPoint(-0.6, -10), + buildPoint(0, 0), + buildPoint(0.6, 10), + buildPoint(1.2, 20), + buildPoint(1.8, 30), + buildPoint(2.4, 40), + buildPoint(3, 50) + ]; + const renderingConfig3 = { + chartType: ChartSeriesType.LINE, + lineWidth: 2 + }; + if (opt_args && opt_args.stepGraph !== undefined) { + renderingConfig3.stepGraph = opt_args.stepGraph; + } + const series3 = new ChartSeries(points3, seriesYAxis2, renderingConfig3); + seriesList.push(series3); + } + + const track = new ChartTrack(viewport); + track.series = seriesList; + + return track; + } + + function buildDashboardTrack(opt_viewport) { + const viewport = opt_viewport || new Viewport( + document.createElement('div')); + + const seriesYAxis = new ChartSeriesYAxis(0, 1.1); + const fileUrl = '/test_data/dashboard_test_points.json'; + const pointsArray = JSON.parse(tr.b.getSync(fileUrl)); + const points = []; + for (let i = 0; i < pointsArray.length; i++) { + points.push(buildPoint(pointsArray[i][0], pointsArray[i][1])); + } + const renderingConfig = { + chartType: ChartSeriesType.LINE, + lineWidth: 1, + stepGraph: false, + selectedPointSize: 10, + solidSelectedDots: true, + highDetail: false, + skipDistance: 0.4 + }; + const series = new ChartSeries(points, seriesYAxis, renderingConfig); + + const track = new ChartTrack(viewport); + track.series = [series]; + + return track; + } + + test('instantiate_lowDetailsWithoutSelection', function() { + const div = document.createElement('div'); + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = buildTrack({viewport}); + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + const dt = new tr.ui.TimelineDisplayTransform(); + const pixelRatio = window.devicePixelRatio || 1; + dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio); + track.viewport.setDisplayTransformImmediately(dt); + + track.height = '100px'; + }); + + test('instantiate_highDetailsWithSelection', function() { + const div = document.createElement('div'); + const viewport = new Viewport(div); + viewport.highDetails = true; + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = buildTrack({viewport}); + Polymer.dom(drawingContainer).appendChild(track); + + track.series[0].points[1].modelItem.selectionState = + SelectionState.SELECTED; + track.series[1].points[1].modelItem.selectionState = + SelectionState.SELECTED; + track.series[2].points[3].modelItem.selectionState = + SelectionState.SELECTED; + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + const dt = new tr.ui.TimelineDisplayTransform(); + const pixelRatio = window.devicePixelRatio || 1; + dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio); + track.viewport.setDisplayTransformImmediately(dt); + + track.height = '100px'; + }); + + test('instantiate_lowDetailsNoStepGraphWithoutSelection', function() { + const div = document.createElement('div'); + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = buildTrack({viewport, stepGraph: false}); + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + const dt = new tr.ui.TimelineDisplayTransform(); + const pixelRatio = window.devicePixelRatio || 1; + dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio); + track.viewport.setDisplayTransformImmediately(dt); + + track.height = '100px'; + }); + + test('instantiate_highDetailsNoStepGraphWithSelection', function() { + const div = document.createElement('div'); + const viewport = new Viewport(div); + viewport.highDetails = true; + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = buildTrack({viewport, stepGraph: false}); + Polymer.dom(drawingContainer).appendChild(track); + + track.series[0].points[1].modelItem.selectionState = + SelectionState.SELECTED; + track.series[1].points[1].modelItem.selectionState = + SelectionState.SELECTED; + track.series[2].points[3].modelItem.selectionState = + SelectionState.SELECTED; + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + const dt = new tr.ui.TimelineDisplayTransform(); + const pixelRatio = window.devicePixelRatio || 1; + dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio); + track.viewport.setDisplayTransformImmediately(dt); + + track.height = '100px'; + }); + + test('instantiate_highDetailsNoStepGraphWithSelectionAndYAxisLabels', + function() { + const div = document.createElement('div'); + const viewport = new Viewport(div); + viewport.highDetails = true; + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = buildTrack({ + viewport, + stepGraph: false, + singleAxis: true, + }); + track.showYAxisLabels = true; + Polymer.dom(drawingContainer).appendChild(track); + + track.series[0].points[1].modelItem.selectionState = + SelectionState.SELECTED; + track.series[1].points[1].modelItem.selectionState = + SelectionState.SELECTED; + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + const dt = new tr.ui.TimelineDisplayTransform(); + const pixelRatio = window.devicePixelRatio || 1; + dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio); + track.viewport.setDisplayTransformImmediately(dt); + + track.height = '200px'; + }); + + test('instantiate_highDetailsNoStepGraphWithSelectionAndGridLines', + function() { + const div = document.createElement('div'); + const viewport = new Viewport(div); + viewport.highDetails = true; + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = buildTrack({ + viewport, + stepGraph: false, + singleAxis: true, + }); + track.showGridLines = true; + Polymer.dom(drawingContainer).appendChild(track); + + track.series[0].points[1].modelItem.selectionState = + SelectionState.SELECTED; + track.series[1].points[1].modelItem.selectionState = + SelectionState.SELECTED; + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + const dt = new tr.ui.TimelineDisplayTransform(); + const pixelRatio = window.devicePixelRatio || 1; + dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio); + track.viewport.setDisplayTransformImmediately(dt); + + track.height = '200px'; + }); + + test('instantiate_highDetailsNoStepGraphWithSelectionYAxisLabelsAndGridLines', + function() { + const div = document.createElement('div'); + const viewport = new Viewport(div); + viewport.highDetails = true; + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = buildTrack({ + viewport, + stepGraph: false, + singleAxis: true, + }); + track.showYAxisLabels = true; + track.showGridLines = true; + Polymer.dom(drawingContainer).appendChild(track); + + track.series[0].points[1].modelItem.selectionState = + SelectionState.SELECTED; + track.series[1].points[1].modelItem.selectionState = + SelectionState.SELECTED; + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + const dt = new tr.ui.TimelineDisplayTransform(); + const pixelRatio = window.devicePixelRatio || 1; + dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio); + track.viewport.setDisplayTransformImmediately(dt); + + track.height = '200px'; + }); + + test('instantiate_dashboardChartStyleWithSelection', function() { + const div = document.createElement('div'); + const viewport = new Viewport(div); + viewport.highDetails = true; + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = buildDashboardTrack(viewport); + track.showYAxisLabels = true; + track.showGridLines = true; + Polymer.dom(drawingContainer).appendChild(track); + + track.series[0].points[40].modelItem.selectionState = + SelectionState.SELECTED; + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + const dt = new tr.ui.TimelineDisplayTransform(); + const pixelRatio = window.devicePixelRatio || 1; + dt.xSetWorldBounds( + 26610390797802200, 28950000891700000, track.clientWidth * pixelRatio); + track.viewport.setDisplayTransformImmediately(dt); + + track.height = '100px'; + }); + + test('checkPadding', function() { + const track = buildTrack(); + + // Padding should be equal to half maximum point size. + assert.strictEqual(track.topPadding_, 5); + assert.strictEqual(track.bottomPadding_, 5); + }); + + test('checkAddEventsToTrackMap', function() { + const track = buildTrack(); + const eventToTrackMap = new EventToTrackMap(); + track.addEventsToTrackMap(eventToTrackMap); + assert.lengthOf(Object.keys(eventToTrackMap), 23); + }); + + test('checkaddIntersectingEventsInRangeToSelectionInWorldSpace', function() { + const track = buildTrack(); + + const sel = new EventSet(); + track.addIntersectingEventsInRangeToSelectionInWorldSpace( + -1.1, -0.7, 0.01, sel); + assert.lengthOf(sel, 3); + const iter = sel[Symbol.iterator](); + assert.strictEqual(iter.next().value, track.series[0].points[1].modelItem); + assert.strictEqual(iter.next().value, track.series[1].points[1].modelItem); + assert.strictEqual(iter.next().value, track.series[2].points[3].modelItem); + }); + + test('checkaddEventNearToProvidedEventToSelection', function() { + const track = buildTrack(); + + // Fail to find a near item to the left in any series. + let sel = new EventSet(); + assert.isFalse(track.addEventNearToProvidedEventToSelection( + track.series[0].points[0].modelItem, -1, sel)); + assert.lengthOf(sel, 0); + + // Succeed at finding a near item to the right of one series. + sel = new EventSet(); + assert.isTrue(track.addEventNearToProvidedEventToSelection( + track.series[1].points[1].modelItem, 1, sel)); + assert.strictEqual( + tr.b.getOnlyElement(sel), track.series[1].points[2].modelItem); + }); + + test('checkAddClosestEventToSelection', function() { + const track = buildTrack(); + + const sel = new EventSet(); + track.addClosestEventToSelection(-0.8, 0.4, 0.5, 1.5, sel); + assert.lengthOf(sel, 2); + const iter = sel[Symbol.iterator](); + assert.strictEqual(iter.next().value, track.series[0].points[2].modelItem); + assert.strictEqual(iter.next().value, track.series[2].points[4].modelItem); + }); + + test('checkAutoSetAllAxes', function() { + const track = buildTrack(); + const seriesYAxis1 = track.series[0].seriesYAxis; + const seriesYAxis2 = track.series[2].seriesYAxis; + + track.autoSetAllAxes({expandMax: true, shrinkMax: true}); + + // Min bounds of both axes should not have been modified. + assert.strictEqual(seriesYAxis1.bounds.min, 0); + assert.strictEqual(seriesYAxis2.bounds.min, -100); + + // Max bounds of both axes should have been modified. + assert.strictEqual(seriesYAxis1.bounds.max, 2.2); + assert.strictEqual(seriesYAxis2.bounds.max, 50); + }); + + test('checkAutoSetAxis', function() { + const track = buildTrack(); + const seriesYAxis1 = track.series[0].seriesYAxis; + const seriesYAxis2 = track.series[2].seriesYAxis; + + track.autoSetAxis(seriesYAxis2, + {expandMin: true, shrinkMin: true, expandMax: true, shrinkMax: true}); + + // First axis should not have been modified. + assert.strictEqual(seriesYAxis1.bounds.min, 0); + assert.strictEqual(seriesYAxis1.bounds.max, 2.5); + + // Second axis should have been modified. + assert.strictEqual(seriesYAxis2.bounds.min, -50); + assert.strictEqual(seriesYAxis2.bounds.max, 50); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html new file mode 100644 index 00000000000..f6bf6310116 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A helper object encapsulating all parameters necessary to draw a chart + * series and provides conversion between world coordinates and physical + * pixels. + * + * All parameters (except for pixelRatio) are assumed to be in physical pixels + * (i.e. already pre-multiplied with pixelRatio). + * + * The diagram below explains the meaning of the resulting fields with + * respect to a chart track: + * + * outerTopViewY -> +--------------------/-\------+ <- Top padding + * innerTopViewY -> + - - - - - - - - - -| |- - - + <- Axis max + * | .. ==\-/== | + * | === Series === | + * | ==/-\== .. | + * innerBottomViewY -> + - - -Point- - - - - - - - - + <- Axis min + * outerBottomViewY -> +-------\-/-------------------+ <- Bottom padding + * ^ ^ + * leftViewX rightViewX + * leftTimeStamp rightTimestamp + * + * Labels starting with a lower case letter are the resulting fields of the + * transform object. Labels starting with an upper case letter correspond + * to the relevant chart track concepts. + * + * @constructor + */ + function ChartTransform(displayTransform, axis, trackWidth, + trackHeight, topPadding, bottomPadding, pixelRatio) { + this.pixelRatio = pixelRatio; + + // X axis. + this.leftViewX = 0; + this.rightViewX = trackWidth; + this.leftTimestamp = displayTransform.xViewToWorld(this.leftViewX); + this.rightTimestamp = displayTransform.xViewToWorld(this.rightViewX); + + this.displayTransform_ = displayTransform; + + // Y axis. + this.outerTopViewY = 0; + this.innerTopViewY = topPadding; + this.innerBottomViewY = trackHeight - bottomPadding; + this.outerBottomViewY = trackHeight; + + this.axis_ = axis; + this.innerHeight_ = this.innerBottomViewY - this.innerTopViewY; + } + + ChartTransform.prototype = { + worldXToViewX(worldX) { + return this.displayTransform_.xWorldToView(worldX); + }, + + viewXToWorldX(viewX) { + return this.displayTransform_.xViewToWorld(viewX); + }, + + vectorToWorldDistance(viewY) { + return this.axis_.bounds.range * Math.abs(viewY / this.innerHeight_); + }, + + viewYToWorldY(viewY) { + return this.axis_.unitRangeToValue( + 1 - (viewY - this.innerTopViewY) / this.innerHeight_); + }, + + worldYToViewY(worldY) { + const innerHeightCoefficient = 1 - this.axis_.valueToUnitRange(worldY); + return innerHeightCoefficient * this.innerHeight_ + this.innerTopViewY; + } + }; + + return { + ChartTransform, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html new file mode 100644 index 00000000000..8d46e08aace --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html @@ -0,0 +1,106 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/timeline_display_transform.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html"> +<link rel="import" href="/tracing/ui/tracks/chart_transform.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform; + const ChartTransform = tr.ui.tracks.ChartTransform; + const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis; + + function buildChartTransform() { + const displayTransform = new TimelineDisplayTransform(); + displayTransform.panX = -20; + displayTransform.scaleX = 0.5; + + const seriesYAxis = new ChartSeriesYAxis(-100, 100); + + const chartTransform = new ChartTransform( + displayTransform, + seriesYAxis, + 500, /* trackWidth */ + 80, /* trackHeight */ + 15, /* topPadding */ + 5, /* bottomPadding */ + 3 /* pixelRatio */); + + return chartTransform; + } + + test('checkFields', function() { + const t = buildChartTransform(); + + assert.strictEqual(t.pixelRatio, 3); + + assert.strictEqual(t.leftViewX, 0); + assert.strictEqual(t.rightViewX, 500); + assert.strictEqual(t.leftTimestamp, 20); + assert.strictEqual(t.rightTimestamp, 1020); + + assert.strictEqual(t.outerTopViewY, 0); + assert.strictEqual(t.innerTopViewY, 15); + assert.strictEqual(t.innerBottomViewY, 75); + assert.strictEqual(t.outerBottomViewY, 80); + }); + + test('checkWorldXToViewX', function() { + const t = buildChartTransform(); + + assert.strictEqual(t.worldXToViewX(-100), -60); + assert.strictEqual(t.worldXToViewX(0), -10); + assert.strictEqual(t.worldXToViewX(520), 250); + assert.strictEqual(t.worldXToViewX(1020), 500); + assert.strictEqual(t.worldXToViewX(1200), 590); + }); + + test('checkViewXToWorldX', function() { + const t = buildChartTransform(); + + assert.strictEqual(t.viewXToWorldX(-60), -100); + assert.strictEqual(t.viewXToWorldX(-10), 0); + assert.strictEqual(t.viewXToWorldX(250), 520); + assert.strictEqual(t.viewXToWorldX(500), 1020); + assert.strictEqual(t.viewXToWorldX(590), 1200); + }); + + test('checkWorldYToViewY', function() { + const t = buildChartTransform(); + + assert.strictEqual(t.worldYToViewY(-200), 105); + assert.strictEqual(t.worldYToViewY(-100), 75); + assert.strictEqual(t.worldYToViewY(0), 45); + assert.strictEqual(t.worldYToViewY(100), 15); + assert.strictEqual(t.worldYToViewY(200), -15); + }); + + test('checkViewYToWorldY', function() { + const t = buildChartTransform(); + + assert.strictEqual(t.viewYToWorldY(105), -200); + assert.strictEqual(t.viewYToWorldY(75), -100); + assert.strictEqual(t.viewYToWorldY(45), 0); + assert.strictEqual(t.viewYToWorldY(15), 100); + assert.strictEqual(t.viewYToWorldY(-15), 200); + }); + + test('checkVectorToWorldDistance', function() { + const t = buildChartTransform(); + + assert.strictEqual(t.vectorToWorldDistance(105), 350); + assert.strictEqual(t.vectorToWorldDistance(75), 250); + assert.strictEqual(t.vectorToWorldDistance(45), 150); + assert.strictEqual(t.vectorToWorldDistance(15), 50); + assert.strictEqual(t.vectorToWorldDistance(-15), 50); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html new file mode 100644 index 00000000000..ecaac0dd3b1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * ContainerToTrackMap is a class to handle building and accessing a map + * between an EventContainer's stableId and its handling track. + * + * @constructor + */ + function ContainerToTrackMap() { + this.stableIdToTrackMap_ = {}; + } + + ContainerToTrackMap.prototype = { + addContainer(container, track) { + if (!track) { + throw new Error('Must provide a track.'); + } + this.stableIdToTrackMap_[container.stableId] = track; + }, + + clear() { + this.stableIdToTrackMap_ = {}; + }, + + getTrackByStableId(stableId) { + return this.stableIdToTrackMap_[stableId]; + } + }; + + return { + ContainerToTrackMap, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html new file mode 100644 index 00000000000..454c1df585c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html @@ -0,0 +1,138 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/task.html"> +<link rel="import" href="/tracing/core/filter.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const Task = tr.b.Task; + + /** + * A generic track that contains other tracks as its children. + * @constructor + */ + const ContainerTrack = tr.ui.b.define('container-track', tr.ui.tracks.Track); + ContainerTrack.prototype = { + __proto__: tr.ui.tracks.Track.prototype, + + decorate(viewport) { + tr.ui.tracks.Track.prototype.decorate.call(this, viewport); + }, + + detach() { + Polymer.dom(this).textContent = ''; + }, + + get tracks_() { + const tracks = []; + for (let i = 0; i < this.children.length; i++) { + if (this.children[i] instanceof tr.ui.tracks.Track) { + tracks.push(this.children[i]); + } + } + return tracks; + }, + + drawTrack(type) { + this.tracks_.forEach(function(track) { + track.drawTrack(type); + }); + }, + + /** + * Adds items intersecting the given range to a selection. + * @param {number} loVX Lower X bound of the interval to search, in + * viewspace. + * @param {number} hiVX Upper X bound of the interval to search, in + * viewspace. + * @param {number} loY Lower Y bound of the interval to search, in + * viewspace space. + * @param {number} hiY Upper Y bound of the interval to search, in + * viewspace space. + * @param {Selection} selection Selection to which to add results. + */ + addIntersectingEventsInRangeToSelection( + loVX, hiVX, loY, hiY, selection) { + for (let i = 0; i < this.tracks_.length; i++) { + const trackClientRect = this.tracks_[i].getBoundingClientRect(); + const a = Math.max(loY, trackClientRect.top); + const b = Math.min(hiY, trackClientRect.bottom); + if (a <= b) { + this.tracks_[i].addIntersectingEventsInRangeToSelection( + loVX, hiVX, loY, hiY, selection); + } + } + + tr.ui.tracks.Track.prototype.addIntersectingEventsInRangeToSelection. + apply(this, arguments); + }, + + addEventsToTrackMap(eventToTrackMap) { + for (const track of this.tracks_) { + track.addEventsToTrackMap(eventToTrackMap); + } + }, + + addAllEventsMatchingFilterToSelection(filter, selection) { + for (let i = 0; i < this.tracks_.length; i++) { + this.tracks_[i].addAllEventsMatchingFilterToSelection( + filter, selection); + } + }, + + addAllEventsMatchingFilterToSelectionAsTask(filter, selection) { + const task = new Task(); + for (let i = 0; i < this.tracks_.length; i++) { + task.subTask(function(i) { + return function() { + this.tracks_[i].addAllEventsMatchingFilterToSelection( + filter, selection); + }; + }(i), this); + } + return task; + }, + + addClosestEventToSelection( + worldX, worldMaxDist, loY, hiY, selection) { + for (let i = 0; i < this.tracks_.length; i++) { + const trackClientRect = this.tracks_[i].getBoundingClientRect(); + const a = Math.max(loY, trackClientRect.top); + const b = Math.min(hiY, trackClientRect.bottom); + if (a <= b) { + this.tracks_[i].addClosestEventToSelection( + worldX, worldMaxDist, loY, hiY, selection); + } + } + + tr.ui.tracks.Track.prototype.addClosestEventToSelection. + apply(this, arguments); + }, + + addContainersToTrackMap(containerToTrackMap) { + this.tracks_.forEach(function(track) { + track.addContainersToTrackMap(containerToTrackMap); + }); + }, + + clearTracks_() { + this.tracks_.forEach(function(track) { + Polymer.dom(this).removeChild(track); + }, this); + } + }; + + return { + ContainerTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html new file mode 100644 index 00000000000..7f25e41bf6a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/chart_point.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html"> +<link rel="import" href="/tracing/ui/tracks/chart_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays a Counter object. + * @constructor + * @extends {ChartTrack} + */ + const CounterTrack = tr.ui.b.define('counter-track', tr.ui.tracks.ChartTrack); + + CounterTrack.prototype = { + __proto__: tr.ui.tracks.ChartTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.ChartTrack.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('counter-track'); + }, + + get counter() { + return this.chart; + }, + + set counter(counter) { + this.heading = counter.name + ': '; + this.series = CounterTrack.buildChartSeriesFromCounter(counter); + this.autoSetAllAxes({expandMax: true}); + }, + + getModelEventFromItem(chartValue) { + return chartValue; + } + }; + + CounterTrack.buildChartSeriesFromCounter = function(counter) { + const numSeries = counter.series.length; + const totals = counter.totals; + + // Create one common axis for all series. + const seriesYAxis = new tr.ui.tracks.ChartSeriesYAxis(0, undefined); + + // Build one chart series for each counter series. + const chartSeries = counter.series.map(function(series, seriesIndex) { + const chartPoints = series.samples.map(function(sample, sampleIndex) { + const total = totals[sampleIndex * numSeries + seriesIndex]; + return new tr.ui.tracks.ChartPoint(sample, sample.timestamp, total); + }); + const renderingConfig = { + chartType: tr.ui.tracks.ChartSeriesType.AREA, + colorId: series.color + }; + return new tr.ui.tracks.ChartSeries( + chartPoints, seriesYAxis, renderingConfig); + }); + + // Show the first series (with the smallest cumulative value) at the top. + chartSeries.reverse(); + + return chartSeries; + }; + + return { + CounterTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html new file mode 100644 index 00000000000..3a4f84a14b4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html @@ -0,0 +1,129 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/full_config.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function getSynchronous(url) { + const req = new XMLHttpRequest(); + req.open('GET', url, false); + // Without the mime type specified like this, the file's bytes are not + // retrieved correctly. + req.overrideMimeType('text/plain; charset=x-user-defined'); + req.send(null); + return req.responseText; + } + + const ZOOM_STEPS = 10; + const ZOOM_COEFFICIENT = 1.2; + + let model = undefined; + + let drawingContainer; + let viewportDiv; + + let viewportWidth; + let worldMid; + + let startScale = undefined; + + function timedCounterTrackPerfTest(name, testFn, iterations) { + function setUpOnce() { + if (model !== undefined) return; + + const fileUrl = '/test_data/counter_tracks.html'; + const events = getSynchronous(fileUrl); + model = tr.c.TestUtils.newModelWithEvents([events]); + } + + function setUp() { + setUpOnce(); + viewportDiv = document.createElement('div'); + + const viewport = new tr.ui.TimelineViewport(viewportDiv); + + drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + viewport.modelTrackContainer = drawingContainer; + + const modelTrack = new tr.ui.tracks.ModelTrack(viewport); + Polymer.dom(drawingContainer).appendChild(modelTrack); + + modelTrack.model = model; + + Polymer.dom(viewportDiv).appendChild(drawingContainer); + + this.addHTMLOutput(viewportDiv); + + // Size the canvas. + drawingContainer.updateCanvasSizeIfNeeded_(); + + // Size the viewport. + viewportWidth = drawingContainer.canvas.width; + const min = model.bounds.min; + const range = model.bounds.range; + worldMid = min + range / 2; + + const boost = range * 0.15; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(min - boost, min + range + boost, viewportWidth); + modelTrack.viewport.setDisplayTransformImmediately(dt); + startScale = dt.scaleX; + + // Select half of the counter samples. + for (const pid in model.processes) { + const counters = model.processes[pid].counters; + for (const cid in counters) { + const series = counters[cid].series; + for (let i = 0; i < series.length; i++) { + const samples = series[i].samples; + for (let j = Math.floor(samples.length / 2); j < samples.length; + j++) { + samples[j].selectionState = + tr.model.SelectionState.SELECTED; + } + } + } + } + } + + function tearDown() { + viewportDiv.innerText = ''; + drawingContainer = undefined; + } + + timedPerfTest(name, testFn, { + setUp, + tearDown, + iterations + }); + } + + const n110100 = [1, 10, 100]; + n110100.forEach(function(val) { + timedCounterTrackPerfTest( + 'draw_softwareCanvas_' + val, + function() { + let scale = startScale; + for (let i = 0; i < ZOOM_STEPS; i++) { + const dt = + drawingContainer.viewport.currentDisplayTransform.clone(); + scale *= ZOOM_COEFFICIENT; + dt.scaleX = scale; + dt.xPanWorldPosToViewPos(worldMid, 'center', viewportWidth); + drawingContainer.viewport.setDisplayTransformImmediately(dt); + drawingContainer.draw_(); + } + }, val); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html new file mode 100644 index 00000000000..dd0286b6b67 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html @@ -0,0 +1,205 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ColorScheme = tr.b.ColorScheme; + const Counter = tr.model.Counter; + const Viewport = tr.ui.TimelineViewport; + const CounterTrack = tr.ui.tracks.CounterTrack; + + const runTest = function(timestamps, samples, testFn) { + const testEl = document.createElement('div'); + + const ctr = new Counter(undefined, 'foo', '', 'foo'); + const n = samples.length; + + for (let i = 0; i < n; ++i) { + ctr.addSeries(new tr.model.CounterSeries('value' + i, + ColorScheme.getColorIdForGeneralPurposeString('value' + i))); + } + + for (let i = 0; i < samples.length; ++i) { + for (let k = 0; k < timestamps.length; ++k) { + ctr.series[i].addCounterSample(timestamps[k], samples[i][k]); + } + } + + ctr.updateBounds(); + + const viewport = new Viewport(testEl); + + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(testEl).appendChild(drawingContainer); + + const track = new CounterTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + this.addHTMLOutput(testEl); + + // Force the container to update sizes so the test can use coordinates that + // make sense. This has to be after the adding of the track as we need to + // use the track header to figure out our positioning. + drawingContainer.updateCanvasSizeIfNeeded_(); + + const pixelRatio = window.devicePixelRatio || 1; + + track.heading = ctr.name; + track.counter = ctr; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 10, track.clientWidth * pixelRatio); + track.viewport.setDisplayTransformImmediately(dt); + + testFn(ctr, drawingContainer, track); + }; + + test('instantiate', function() { + const ctr = new Counter(undefined, 'testBasicCounter', '', + 'testBasicCounter'); + ctr.addSeries(new tr.model.CounterSeries('value1', + ColorScheme.getColorIdForGeneralPurposeString( + 'testBasicCounter.value1'))); + ctr.addSeries(new tr.model.CounterSeries('value2', + ColorScheme.getColorIdForGeneralPurposeString( + 'testBasicCounter.value2'))); + + const timestamps = [0, 1, 2, 3, 4, 5, 6, 7]; + const samples = [[0, 3, 1, 2, 3, 1, 3, 3.1], + [5, 3, 1, 1.1, 0, 7, 0, 0.5]]; + for (let i = 0; i < samples.length; ++i) { + for (let k = 0; k < timestamps.length; ++k) { + ctr.series[i].addCounterSample(timestamps[k], samples[i][k]); + } + } + + ctr.updateBounds(); + + const div = document.createElement('div'); + const viewport = new Viewport(div); + + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = new CounterTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + track.heading = ctr.name; + track.counter = ctr; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 7.7, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + }); + + test('basicCounterXPointPicking', function() { + const timestamps = [0, 1, 2, 3, 4, 5, 6, 7]; + const samples = [[0, 3, 1, 2, 3, 1, 3, 3.1], + [5, 3, 1, 1.1, 0, 7, 0, 0.5]]; + + runTest.call(this, timestamps, samples, function(ctr, container, track) { + const clientRect = track.getBoundingClientRect(); + const y75 = clientRect.top + (0.75 * clientRect.height); + + // In bounds. + let sel = new tr.model.EventSet(); + let x = 0.15 * clientRect.width; + track.addIntersectingEventsInRangeToSelection( + x, x + 1, y75, y75 + 1, sel); + + let nextSeriesIndex = 1; + assert.strictEqual(sel.length, 2); + for (const event of sel) { + assert.strictEqual(event.series.counter, ctr); + assert.strictEqual(event.getSampleIndex(), 1); + assert.strictEqual(event.series.seriesIndex, nextSeriesIndex--); + } + + // Outside bounds. + sel = new tr.model.EventSet(); + x = -0.5 * clientRect.width; + track.addIntersectingEventsInRangeToSelection( + x, x + 1, y75, y75 + 1, sel); + assert.strictEqual(sel.length, 0); + + sel = new tr.model.EventSet(); + x = 0.8 * clientRect.width; + track.addIntersectingEventsInRangeToSelection( + x, x + 1, y75, y75 + 1, sel); + assert.strictEqual(sel.length, 0); + }); + }); + + test('counterTrackAddClosestEventToSelection', function() { + const timestamps = [0, 1, 2, 3, 4, 5, 6, 7]; + const samples = [[0, 4, 1, 2, 3, 1, 3, 3.1], + [5, 3, 1, 1.1, 0, 7, 0, 0.5]]; + + runTest.call(this, timestamps, samples, function(ctr, container, track) { + // Before with not range. + let sel = new tr.model.EventSet(); + track.addClosestEventToSelection(-1, 0, 0, 0, sel); + assert.strictEqual(sel.length, 0); + + // Before with negative range. + sel = new tr.model.EventSet(); + track.addClosestEventToSelection(-1, -10, 0, 0, sel); + assert.strictEqual(sel.length, 0); + + // Before first sample. + sel = new tr.model.EventSet(); + track.addClosestEventToSelection(-1, 1, 0, 0, sel); + assert.strictEqual(sel.length, 2); + for (const event of sel) { + assert.strictEqual(event.getSampleIndex(), 0); + } + + // Between and closer to sample before. + sel = new tr.model.EventSet(); + track.addClosestEventToSelection(1.3, 1, 0, 0, sel); + assert.strictEqual(sel.length, 2); + for (const event of sel) { + assert.strictEqual(event.getSampleIndex(), 1); + } + + // Between samples with bad range. + sel = new tr.model.EventSet(); + track.addClosestEventToSelection(1.45, 0.25, 0, 0, sel); + assert.strictEqual(sel.length, 0); + + // Between and closer to next sample. + sel = new tr.model.EventSet(); + track.addClosestEventToSelection(4.7, 6, 0, 0, sel); + assert.strictEqual(sel.length, 2); + for (const event of sel) { + assert.strictEqual(event.getSampleIndex(), 5); + } + + // After last sample with good range. + sel = new tr.model.EventSet(); + track.addClosestEventToSelection(8.5, 2, 0, 0, sel); + assert.strictEqual(sel.length, 2); + for (const event of sel) { + assert.strictEqual(event.getSampleIndex(), 7); + } + + // After last sample with bad range. + sel = new tr.model.EventSet(); + track.addClosestEventToSelection(10, 1, 0, 0, sel); + assert.strictEqual(sel.length, 0); + }); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html new file mode 100644 index 00000000000..3a6c627fb38 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html @@ -0,0 +1,140 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/filter.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/container_track.html"> +<link rel="import" href="/tracing/ui/tracks/slice_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * Visualizes a Cpu using a series of SliceTracks. + * @constructor + */ + const CpuTrack = + tr.ui.b.define('cpu-track', tr.ui.tracks.ContainerTrack); + CpuTrack.prototype = { + __proto__: tr.ui.tracks.ContainerTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('cpu-track'); + this.detailedMode_ = true; + }, + + get cpu() { + return this.cpu_; + }, + + set cpu(cpu) { + this.cpu_ = cpu; + this.updateContents_(); + }, + + get detailedMode() { + return this.detailedMode_; + }, + + set detailedMode(detailedMode) { + this.detailedMode_ = detailedMode; + this.updateContents_(); + }, + + get tooltip() { + return this.tooltip_; + }, + + set tooltip(value) { + this.tooltip_ = value; + this.updateContents_(); + }, + + get hasVisibleContent() { + if (this.cpu_ === undefined) return false; + + const cpu = this.cpu_; + if (cpu.slices.length) return true; + + if (cpu.samples && cpu.samples.length) return true; + + if (Object.keys(cpu.counters).length > 0) return true; + + return false; + }, + + updateContents_() { + this.detach(); + if (!this.cpu_) return; + + const slices = this.cpu_.slices; + if (slices.length) { + const track = new tr.ui.tracks.SliceTrack(this.viewport); + track.slices = slices; + track.heading = this.cpu_.userFriendlyName + ':'; + Polymer.dom(this).appendChild(track); + } + + if (this.detailedMode_) { + this.appendSamplesTracks_(); + + for (const counterName in this.cpu_.counters) { + const counter = this.cpu_.counters[counterName]; + const track = new tr.ui.tracks.CounterTrack(this.viewport); + track.heading = this.cpu_.userFriendlyName + ' ' + + counter.name + ':'; + track.counter = counter; + Polymer.dom(this).appendChild(track); + } + } + }, + + appendSamplesTracks_() { + const samples = this.cpu_.samples; + if (samples === undefined || samples.length === 0) { + return; + } + const samplesByTitle = {}; + samples.forEach(function(sample) { + if (samplesByTitle[sample.title] === undefined) { + samplesByTitle[sample.title] = []; + } + samplesByTitle[sample.title].push(sample); + }); + + const sampleTitles = Object.keys(samplesByTitle); + sampleTitles.sort(); + + sampleTitles.forEach(function(sampleTitle) { + const samples = samplesByTitle[sampleTitle]; + const samplesTrack = new tr.ui.tracks.SliceTrack(this.viewport); + samplesTrack.group = this.cpu_; + samplesTrack.slices = samples; + samplesTrack.heading = this.cpu_.userFriendlyName + ': ' + + sampleTitle; + samplesTrack.tooltip = this.cpu_.userFriendlyDetails; + samplesTrack.selectionGenerator = function() { + const selection = new tr.model.EventSet(); + for (let i = 0; i < samplesTrack.slices.length; i++) { + selection.push(samplesTrack.slices[i]); + } + return selection; + }; + Polymer.dom(this).appendChild(samplesTrack); + }, this); + } + }; + + return { + CpuTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html new file mode 100644 index 00000000000..442992522f5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Cpu = tr.model.Cpu; + const CpuTrack = tr.ui.tracks.CpuTrack; + const ThreadSlice = tr.model.ThreadSlice; + const StackFrame = tr.model.StackFrame; + const Sample = tr.model.Sample; + const Thread = tr.model.Thread; + const Viewport = tr.ui.TimelineViewport; + + test('basicCpu', function() { + const cpu = new Cpu({}, 7); + cpu.slices = [ + new ThreadSlice('', 'a', 0, 1, {}, 1), + new ThreadSlice('', 'b', 1, 2.1, {}, 4.8) + ]; + cpu.updateBounds(); + + const testEl = document.createElement('div'); + const viewport = new Viewport(testEl); + + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + + const track = new CpuTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + + track.heading = 'CPU ' + cpu.cpuNumber; + track.cpu = cpu; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 11.1, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + }); + + + test('withSamples', function() { + let thread; + let cpu; + const model = tr.c.TestUtils.newModelWithEvents([], { + shiftWorldToZero: false, + pruneContainers: false, + customizeModelCallback(model) { + cpu = model.kernel.getOrCreateCpu(1); + thread = model.getOrCreateProcess(1).getOrCreateThread(2); + + const nodeA = tr.c.TestUtils.newProfileNode(model, 'a'); + const nodeB = tr.c.TestUtils.newProfileNode(model, 'b', nodeA); + const nodeC = tr.c.TestUtils.newProfileNode(model, 'c', nodeB); + const nodeD = tr.c.TestUtils.newProfileNode(model, 'd', nodeA); + + model.samples.push(new Sample(10, 'instructions_retired', nodeC, thread, + undefined, 10)); + model.samples.push(new Sample(20, 'instructions_retired', nodeB, thread, + undefined, 10)); + model.samples.push(new Sample(30, 'instructions_retired', nodeB, thread, + undefined, 10)); + model.samples.push(new Sample(40, 'instructions_retired', nodeD, thread, + undefined, 10)); + + model.samples.push(new Sample(25, 'page_fault', nodeB, thread, + undefined, 10)); + model.samples.push(new Sample(35, 'page_fault', nodeD, thread, + undefined, 10)); + } + }); + + const testEl = document.createElement('div'); + const viewport = new Viewport(testEl); + + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + + const track = new CpuTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + + track.heading = 'CPU ' + cpu.cpuNumber; + track.cpu = cpu; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 11.1, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html new file mode 100644 index 00000000000..912220b8236 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/chart_point.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html"> +<link rel="import" href="/tracing/ui/tracks/chart_track.html"> + +<style> +.cpu-usage-track { + height: 90px; +} +</style> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const ColorScheme = tr.b.ColorScheme; + const ChartTrack = tr.ui.tracks.ChartTrack; + + /** + * A track that displays the cpu usage of a process. + * + * @constructor + * @extends {tr.ui.tracks.ChartTrack} + */ + const CpuUsageTrack = tr.ui.b.define('cpu-usage-track', ChartTrack); + + CpuUsageTrack.prototype = { + __proto__: ChartTrack.prototype, + + decorate(viewport) { + ChartTrack.prototype.decorate.call(this, viewport); + this.classList.add('cpu-usage-track'); + this.heading = 'CPU usage'; + this.cpuUsageSeries_ = undefined; + }, + + // Given a tr.Model, it creates a cpu usage series and a graph. + initialize(model) { + if (model !== undefined) { + this.cpuUsageSeries_ = model.device.cpuUsageSeries; + } else { + this.cpuUsageSeries_ = undefined; + } + this.series = this.buildChartSeries_(); + this.autoSetAllAxes({expandMax: true}); + }, + + get hasVisibleContent() { + return !!this.cpuUsageSeries_ && + this.cpuUsageSeries_.samples.length > 0; + }, + + addContainersToTrackMap(containerToTrackMap) { + containerToTrackMap.addContainer(this.series_, this); + }, + + buildChartSeries_(yAxis, color) { + if (!this.hasVisibleContent) return []; + + yAxis = new tr.ui.tracks.ChartSeriesYAxis(0, undefined); + const usageSamples = this.cpuUsageSeries_.samples; + const pts = new Array(usageSamples.length + 1); + for (let i = 0; i < usageSamples.length; i++) { + pts[i] = new tr.ui.tracks.ChartPoint(undefined, + usageSamples[i].start, usageSamples[i].usage); + } + pts[usageSamples.length] = new tr.ui.tracks.ChartPoint(undefined, + usageSamples[usageSamples.length - 1].start, 0); + const renderingConfig = { + chartType: tr.ui.tracks.ChartSeriesType.AREA, + colorId: color + }; + + return [new tr.ui.tracks.ChartSeries(pts, yAxis, renderingConfig)]; + }, + }; + + return { + CpuUsageTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html new file mode 100644 index 00000000000..2970e81eaf8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html @@ -0,0 +1,215 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/cpu/cpu_usage_auditor.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/thread_slice.html"> +<link rel='import' href='/tracing/ui/base/constants.html'> +<link rel='import' href='/tracing/ui/timeline_viewport.html'> +<link rel="import" href="/tracing/ui/tracks/cpu_usage_track.html"> +<link rel='import' href='/tracing/ui/tracks/drawing_container.html'> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const ThreadSlice = tr.model.ThreadSlice; + const DIFF_EPSILON = 0.0001; + + // Input : slices is an array-of-array-of slices. Each top level array + // represents a process. So, each slice in one of the top level array + // will be placed in the same process. + function buildModel(slices) { + const model = tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + for (let i = 0; i < slices.length; i++) { + const thread = process.getOrCreateThread(i); + slices[i].forEach(s => thread.sliceGroup.pushSlice(s)); + } + }); + const auditor = new tr.e.audits.CpuUsageAuditor(model); + auditor.runAnnotate(); + return model; + } + + // Compare float arrays based on an epsilon since floating point arithmetic + // is not always 100% accurate. + function assertArrayValuesCloseTo(actualValue, expectedValue) { + assert.lengthOf(actualValue, expectedValue.length); + for (let i = 0; i < expectedValue.length; i++) { + assert.closeTo(actualValue[i], expectedValue[i], DIFF_EPSILON); + } + } + + function createCpuUsageTrack(model, interval) { + const div = document.createElement('div'); + const viewport = new tr.ui.TimelineViewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + div.appendChild(drawingContainer); + const track = new tr.ui.tracks.CpuUsageTrack(drawingContainer.viewport); + if (model !== undefined) { + setDisplayTransformFromBounds(viewport, model.bounds); + } + track.initialize(model, interval); + drawingContainer.appendChild(track); + this.addHTMLOutput(drawingContainer); + return track; + } + + /** + * Sets the mapping between the input range of timestamps and the output range + * of horizontal pixels. + */ + function setDisplayTransformFromBounds(viewport, bounds) { + const dt = new tr.ui.TimelineDisplayTransform(); + const pixelRatio = window.devicePixelRatio || 1; + const chartPixelWidth = + (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio; + dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth); + viewport.setDisplayTransformImmediately(dt); + } + + test('computeCpuUsage_simple', function() { + // Set the boundaries, from 0-15 ms. This slice will not + // contain any CPU usage data, it's just to make the boundaries + // of the bins go as 0-1, 1-2, 2-3, etc. This also tests whether + // this function works properly in the presence of slices that + // don't include CPU usage data. + const bigSlice = new tr.model.ThreadSlice('', title, 0, 0, {}, 15); + // First thread. + // 0 5 10 15 + // [ sliceA ] + // [ sliceB ] [C ] + const sliceA = new tr.model.ThreadSlice('', title, 0, 0.5, {}, 5); + sliceA.cpuDuration = 5; + const sliceB = new tr.model.ThreadSlice('', title, 0, 2.5, {}, 8); + sliceB.cpuDuration = 6; + // The slice completely fits into an interval and is the last. + const sliceC = new tr.model.ThreadSlice('', title, 0, 12.5, {}, 2); + sliceC.cpuDuration = 1; + + // Second thread. + // 0 5 10 15 + // [ sliceD ][ sliceE ] + const sliceD = new tr.model.ThreadSlice('', title, 0, 3.5, {}, 3); + sliceD.cpuDuration = 3; + const sliceE = new tr.model.ThreadSlice('', title, 0, 6.5, {}, 6); + sliceE.cpuDuration = 3; + + const model = buildModel([ + [bigSlice, sliceA, sliceB, sliceC], + [sliceD, sliceE] + ]); + + // Compute average CPU usage over A (but not over B and C). + const avgCpuUsageA = sliceA.cpuSelfTime / sliceA.selfTime; + // Compute average CPU usage over B, C, D, E. They don't have subslices. + const avgCpuUsageB = sliceB.cpuDuration / sliceB.duration; + const avgCpuUsageC = sliceC.cpuDuration / sliceC.duration; + const avgCpuUsageD = sliceD.cpuDuration / sliceD.duration; + const avgCpuUsageE = sliceE.cpuDuration / sliceE.duration; + + const expectedValue = [ + 0, + avgCpuUsageA, + avgCpuUsageA, + avgCpuUsageA + avgCpuUsageB, + avgCpuUsageA + avgCpuUsageB + avgCpuUsageD, + avgCpuUsageA + avgCpuUsageB + avgCpuUsageD, + avgCpuUsageB + avgCpuUsageD, + avgCpuUsageB + avgCpuUsageE, + avgCpuUsageB + avgCpuUsageE, + avgCpuUsageB + avgCpuUsageE, + avgCpuUsageB + avgCpuUsageE, + avgCpuUsageE, + avgCpuUsageE, + avgCpuUsageC, + avgCpuUsageC, + 0 + ]; + const track = createCpuUsageTrack.call(this, model); + const actualValue = track.series[0].points.map(point => point.y); + assertArrayValuesCloseTo(actualValue, expectedValue); + }); + + test('computeCpuUsage_longDurationThreadSlice', function() { + // Create a slice covering 24 hours. + const sliceA = new tr.model.ThreadSlice( + '', title, 0, 0, {}, 24 * 60 * 60 * 1000); + sliceA.cpuDuration = sliceA.duration * 0.25; + + const model = buildModel([[sliceA]]); + + const track = createCpuUsageTrack.call(this, model); + const cpuSamples = track.series[0].points.map(point => point.y); + + // All except the last sample is 0.25, since sliceA.cpuDuration was set to + // 0.25 of the total. + for (const cpuSample of cpuSamples.slice(0, cpuSamples.length - 1)) { + assert.closeTo(cpuSample, 0.25, DIFF_EPSILON); + } + // The last sample is 0. + assert.closeTo(cpuSamples[cpuSamples.length - 1], 0, DIFF_EPSILON); + }); + + test('instantiate', function() { + const sliceA = new tr.model.ThreadSlice('', title, 0, 5.5111, {}, 47.1023); + sliceA.cpuDuration = 25; + const sliceB = new tr.model.ThreadSlice('', title, 0, 11.2384, {}, 1.8769); + sliceB.cpuDuration = 1.5; + const sliceC = new tr.model.ThreadSlice('', title, 0, 11.239, {}, 5.8769); + sliceC.cpuDuration = 5; + const sliceD = new tr.model.ThreadSlice('', title, 0, 48.012, {}, 5.01); + sliceD.cpuDuration = 4; + + const model = buildModel([[sliceA, sliceB, sliceC, sliceD]]); + createCpuUsageTrack.call(this, model); + }); + + test('hasVisibleContent_trueWithThreadSlicePresent', function() { + const sliceA = new tr.model.ThreadSlice('', title, 0, 48.012, {}, 5.01); + sliceA.cpuDuration = 4; + const model = buildModel([[sliceA]]); + const track = createCpuUsageTrack.call(this, model); + + assert.isTrue(track.hasVisibleContent); + }); + + test('hasVisibleContent_falseWithUndefinedProcessModel', function() { + const track = createCpuUsageTrack.call(this, undefined); + + assert.isFalse(track.hasVisibleContent); + }); + + test('hasVisibleContent_falseWithNoThreadSlice', function() { + // model with a CPU and a thread but no ThreadSlice. + const model = buildModel([]); + const track = createCpuUsageTrack.call(this, model); + + assert.isFalse(track.hasVisibleContent); + }); + + test('hasVisibleContent_trueWithSubSlices', function() { + const sliceA = new tr.model.ThreadSlice('', title, 0, 5.5111, {}, 47.1023); + sliceA.cpuDuration = 25; + const sliceB = new tr.model.ThreadSlice('', title, 0, 11.2384, {}, 1.8769); + sliceB.cpuDuration = 1.5; + + const model = buildModel([[sliceA, sliceB]]); + const process = model.getProcess(1); + // B will become lowest level slices of A. + process.getThread(0).sliceGroup.createSubSlices(); + assert.strictEqual( + sliceA.cpuSelfTime, (sliceA.cpuDuration - sliceB.cpuDuration)); + const track = createCpuUsageTrack.call(this, model); + + assert.isTrue(track.hasVisibleContent); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html new file mode 100644 index 00000000000..a068a7ebebb --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html @@ -0,0 +1,90 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/tracks/container_track.html"> +<link rel="import" href="/tracing/ui/tracks/power_series_track.html"> +<link rel="import" href="/tracing/ui/tracks/spacing_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const ContainerTrack = tr.ui.tracks.ContainerTrack; + + // TODO(charliea): Make this track collapsible. + /** + * Track to visualize the device model. + * + * @constructor + * @extends {ContainerTrack} + */ + const DeviceTrack = tr.ui.b.define('device-track', ContainerTrack); + + DeviceTrack.prototype = { + + __proto__: ContainerTrack.prototype, + + decorate(viewport) { + ContainerTrack.prototype.decorate.call(this, viewport); + + Polymer.dom(this).classList.add('device-track'); + this.device_ = undefined; + this.powerSeriesTrack_ = undefined; + }, + + get device() { + return this.device_; + }, + + set device(device) { + this.device_ = device; + this.updateContents_(); + }, + + get powerSeriesTrack() { + return this.powerSeriesTrack_; + }, + + get hasVisibleContent() { + return (this.powerSeriesTrack_ && + this.powerSeriesTrack_.hasVisibleContent); + }, + + addContainersToTrackMap(containerToTrackMap) { + tr.ui.tracks.ContainerTrack.prototype.addContainersToTrackMap.call( + this, containerToTrackMap); + containerToTrackMap.addContainer(this.device, this); + }, + + addEventsToTrackMap(eventToTrackMap) { + this.tracks_.forEach(function(track) { + track.addEventsToTrackMap(eventToTrackMap); + }); + }, + + appendPowerSeriesTrack_() { + this.powerSeriesTrack_ = new tr.ui.tracks.PowerSeriesTrack(this.viewport); + this.powerSeriesTrack_.powerSeries = this.device.powerSeries; + + if (this.powerSeriesTrack_.hasVisibleContent) { + Polymer.dom(this).appendChild(this.powerSeriesTrack_); + Polymer.dom(this).appendChild( + new tr.ui.tracks.SpacingTrack(this.viewport)); + } + }, + + updateContents_() { + this.clearTracks_(); + this.appendPowerSeriesTrack_(); + } + }; + + return { + DeviceTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html new file mode 100644 index 00000000000..fdd3b392993 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html @@ -0,0 +1,145 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel='import' href='/tracing/model/device.html'> +<link rel='import' href='/tracing/model/model.html'> +<link rel="import" href="/tracing/ui/base/constants.html"> +<link rel='import' href='/tracing/ui/timeline_display_transform.html'> +<link rel='import' href='/tracing/ui/timeline_viewport.html'> +<link rel='import' href='/tracing/ui/tracks/device_track.html'> +<link rel='import' href='/tracing/ui/tracks/drawing_container.html'> +<link rel='import' href='/tracing/ui/tracks/event_to_track_map.html'> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Device = tr.model.Device; + const DeviceTrack = tr.ui.tracks.DeviceTrack; + const Model = tr.Model; + const PowerSeries = tr.model.PowerSeries; + + const createDrawingContainer = function(series) { + const div = document.createElement('div'); + const viewport = new tr.ui.TimelineViewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + if (series) { + series.updateBounds(); + setDisplayTransformFromBounds(viewport, series.bounds); + } + + return drawingContainer; + }; + + /** + * Sets the mapping between the input range of timestamps and the output range + * of horizontal pixels. + */ + const setDisplayTransformFromBounds = function(viewport, bounds) { + const dt = new tr.ui.TimelineDisplayTransform(); + const pixelRatio = window.devicePixelRatio || 1; + const chartPixelWidth = + (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio; + dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth); + viewport.setDisplayTransformImmediately(dt); + }; + + test('instantiate', function() { + const device = new Device(new Model()); + device.powerSeries = new PowerSeries(device); + device.powerSeries.addPowerSample(0, 1); + device.powerSeries.addPowerSample(0.5, 2); + device.powerSeries.addPowerSample(1, 3); + device.powerSeries.addPowerSample(1.5, 4); + + const drawingContainer = createDrawingContainer(device.powerSeries); + const track = new DeviceTrack(drawingContainer.viewport); + track.device = device; + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(drawingContainer); + }); + + test('instantiate_noPowerSeries', function() { + const device = new Device(new Model()); + + const drawingContainer = createDrawingContainer(device.powerSeries); + const track = new DeviceTrack(drawingContainer.viewport); + track.device = device; + Polymer.dom(drawingContainer).appendChild(track); + + // Adding output should throw due to no visible content. + assert.throw(function() { this.addHTMLOutput(drawingContainer); }); + }); + + test('setDevice_clearsTrackBeforeUpdating', function() { + const device = new Device(new Model()); + device.powerSeries = new PowerSeries(device); + device.powerSeries.addPowerSample(0, 1); + device.powerSeries.addPowerSample(0.5, 2); + device.powerSeries.addPowerSample(1, 3); + device.powerSeries.addPowerSample(1.5, 4); + + const drawingContainer = createDrawingContainer(device.powerSeries); + + // Set the device twice and make sure that this doesn't result in + // the track appearing twice. + const track = new DeviceTrack(drawingContainer.viewport); + track.device = device; + track.device = device; + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(drawingContainer); + + // The device track should still have two subtracks: one counter track and + // one spacing track. + assert.strictEqual(track.tracks_.length, 2); + }); + + test('addContainersToTrackMap', function() { + const device = new Device(new Model()); + device.powerSeries = new PowerSeries(device); + device.powerSeries.addPowerSample(0, 1); + + const drawingContainer = createDrawingContainer(device.series); + const track = new DeviceTrack(drawingContainer.viewport); + track.device = device; + + const containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap(); + track.addContainersToTrackMap(containerToTrackMap); + + assert.strictEqual(containerToTrackMap.getTrackByStableId('Device'), track); + assert.strictEqual( + containerToTrackMap.getTrackByStableId('Device.PowerSeries'), + track.powerSeriesTrack); + }); + + test('addEventsToTrackMap', function() { + const device = new Device(new Model()); + device.powerSeries = new PowerSeries(device); + device.powerSeries.addPowerSample(0, 1); + device.powerSeries.addPowerSample(0.5, 2); + + const div = document.createElement('div'); + const viewport = new tr.ui.TimelineViewport(div); + + const track = new DeviceTrack(viewport); + track.device = device; + + const eventToTrackMap = new tr.ui.tracks.EventToTrackMap(); + track.addEventsToTrackMap(eventToTrackMap); + + const expected = new tr.ui.tracks.EventToTrackMap(); + expected[device.powerSeries.samples[0].guid] = track.powerSeriesTrack; + expected[device.powerSeries.samples[1].guid] = track.powerSeriesTrack; + + assert.deepEqual(eventToTrackMap, expected); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css new file mode 100644 index 00000000000..a8f4d17c91c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css @@ -0,0 +1,18 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +.drawing-container { + display: inline; + overflow: auto; + overflow-x: hidden; + position: relative; +} + +.drawing-container-canvas { + display: block; + pointer-events: none; + position: absolute; + top: 0; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html new file mode 100644 index 00000000000..d13a3a5383c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html @@ -0,0 +1,236 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="stylesheet" href="/tracing/ui/tracks/drawing_container.css"> + +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/ui/base/constants.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const DrawType = { + GENERAL_EVENT: 1, + INSTANT_EVENT: 2, + BACKGROUND: 3, + GRID: 4, + FLOW_ARROWS: 5, + MARKERS: 6, + HIGHLIGHTS: 7, + ANNOTATIONS: 8 + }; + + // Must be > 1.0. This is the maximum multiple by which the size + // of the canvas can exceed the window dimensions. For example + // if window.innerHeight is 1000 and this is 1.4, then the + // largest the canvas height can be set to is 1400px assuming a + // window.devicePixelRatio of 1. + // Currently this value is set rather large to mostly match + // previous behavior & performance. This should be reduced to + // be as small as possible once raw drawing performance is improved + // such that a repaint doesn't incur a large jank + const MAX_OVERSIZE_MULTIPLE = 3.0; + const REDRAW_SLOP = (MAX_OVERSIZE_MULTIPLE - 1) / 2; + + const DrawingContainer = tr.ui.b.define('drawing-container', + tr.ui.tracks.Track); + + DrawingContainer.prototype = { + __proto__: tr.ui.tracks.Track.prototype, + + decorate(viewport) { + tr.ui.tracks.Track.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('drawing-container'); + + this.canvas_ = document.createElement('canvas'); + this.canvas_.className = 'drawing-container-canvas'; + this.canvas_.style.left = tr.ui.b.constants.HEADING_WIDTH + 'px'; + Polymer.dom(this).appendChild(this.canvas_); + + this.ctx_ = this.canvas_.getContext('2d'); + this.offsetY_ = 0; + + this.viewportChange_ = this.viewportChange_.bind(this); + this.viewport.addEventListener('change', this.viewportChange_); + + window.addEventListener('resize', this.windowResized_.bind(this)); + this.addEventListener('scroll', this.scrollChanged_.bind(this)); + }, + + // Needed to support the calls in TimelineTrackView. + get canvas() { + return this.canvas_; + }, + + context() { + return this.ctx_; + }, + + viewportChange_() { + this.invalidate(); + }, + + windowResized_() { + this.invalidate(); + }, + + scrollChanged_() { + if (this.updateOffsetY_()) { + this.invalidate(); + } + }, + + invalidate() { + if (this.rafPending_) return; + + this.rafPending_ = true; + + tr.b.requestPreAnimationFrame(this.preDraw_, this); + }, + + preDraw_() { + this.rafPending_ = false; + this.updateCanvasSizeIfNeeded_(); + + tr.b.requestAnimationFrameInThisFrameIfPossible(this.draw_, this); + }, + + draw_() { + this.ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.height); + + const typesToDraw = [ + DrawType.BACKGROUND, + DrawType.HIGHLIGHTS, + DrawType.GRID, + DrawType.INSTANT_EVENT, + DrawType.GENERAL_EVENT, + DrawType.MARKERS, + DrawType.ANNOTATIONS, + DrawType.FLOW_ARROWS + ]; + + for (const idx in typesToDraw) { + for (let i = 0; i < this.children.length; ++i) { + if (!(this.children[i] instanceof tr.ui.tracks.Track)) { + continue; + } + this.children[i].drawTrack(typesToDraw[idx]); + } + } + + const pixelRatio = window.devicePixelRatio || 1; + const bounds = this.canvas_.getBoundingClientRect(); + const dt = this.viewport.currentDisplayTransform; + const viewLWorld = dt.xViewToWorld(0); + const viewRWorld = dt.xViewToWorld( + bounds.width * pixelRatio); + const viewHeight = bounds.height * pixelRatio; + + this.viewport.drawGridLines( + this.ctx_, viewLWorld, viewRWorld, viewHeight); + }, + + // Update's this.offsetY_, returning true if the value has changed + // and thus a redraw is needed, or false if it did not change. + updateOffsetY_() { + const maxYDelta = window.innerHeight * REDRAW_SLOP; + let newOffset = this.scrollTop - maxYDelta; + if (Math.abs(newOffset - this.offsetY_) <= maxYDelta) return false; + // Now clamp to the valid range. + const maxOffset = this.scrollHeight - + this.canvas_.getBoundingClientRect().height; + newOffset = Math.max(0, Math.min(newOffset, maxOffset)); + if (newOffset !== this.offsetY_) { + this.offsetY_ = newOffset; + return true; + } + return false; + }, + + updateCanvasSizeIfNeeded_() { + const visibleChildTracks = + Array.from(this.children).filter(this.visibleFilter_); + + if (visibleChildTracks.length === 0) { + return; + } + + const thisBounds = this.getBoundingClientRect(); + + const firstChildTrackBounds = + visibleChildTracks[0].getBoundingClientRect(); + const lastChildTrackBounds = + visibleChildTracks[visibleChildTracks.length - 1]. + getBoundingClientRect(); + + const innerWidth = firstChildTrackBounds.width - + tr.ui.b.constants.HEADING_WIDTH; + const innerHeight = Math.min( + lastChildTrackBounds.bottom - firstChildTrackBounds.top, + Math.floor(window.innerHeight * MAX_OVERSIZE_MULTIPLE)); + + const pixelRatio = window.devicePixelRatio || 1; + if (this.canvas_.width !== innerWidth * pixelRatio) { + this.canvas_.width = innerWidth * pixelRatio; + this.canvas_.style.width = innerWidth + 'px'; + } + + if (this.canvas_.height !== innerHeight * pixelRatio) { + this.canvas_.height = innerHeight * pixelRatio; + this.canvas_.style.height = innerHeight + 'px'; + } + + if (this.canvas_.top !== this.offsetY_) { + this.canvas_.top = this.offsetY_; + this.canvas_.style.top = this.offsetY_ + 'px'; + } + }, + + visibleFilter_(element) { + if (!(element instanceof tr.ui.tracks.Track)) return false; + + return window.getComputedStyle(element).display !== 'none'; + }, + + addClosestEventToSelection( + worldX, worldMaxDist, loY, hiY, selection) { + for (let i = 0; i < this.children.length; ++i) { + if (!(this.children[i] instanceof tr.ui.tracks.Track)) { + continue; + } + const trackClientRect = this.children[i].getBoundingClientRect(); + const a = Math.max(loY, trackClientRect.top); + const b = Math.min(hiY, trackClientRect.bottom); + if (a <= b) { + this.children[i].addClosestEventToSelection( + worldX, worldMaxDist, loY, hiY, selection); + } + } + + tr.ui.tracks.Track.prototype.addClosestEventToSelection. + apply(this, arguments); + }, + + addEventsToTrackMap(eventToTrackMap) { + for (let i = 0; i < this.children.length; ++i) { + if (!(this.children[i] instanceof tr.ui.tracks.Track)) { + continue; + } + this.children[i].addEventsToTrackMap(eventToTrackMap); + } + } + }; + + return { + DrawingContainer, + DrawType, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html new file mode 100644 index 00000000000..7b778b1c332 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html @@ -0,0 +1,137 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/xhr.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/full_config.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + let generalModel; + function getOrCreateGeneralModel() { + if (generalModel !== undefined) { + generalModel; + } + const fileUrl = '/test_data/thread_time_visualisation.json.gz'; + const events = tr.b.getSync(fileUrl); + generalModel = tr.c.TestUtils.newModelWithEvents([events]); + return generalModel; + } + + function DCPerfTestCase(testName, opt_options) { + tr.b.unittest.PerfTestCase.call(this, testName, undefined, opt_options); + this.viewportDiv = undefined; + this.drawingContainer = undefined; + this.viewport = undefined; + } + DCPerfTestCase.prototype = { + __proto__: tr.b.unittest.PerfTestCase.prototype, + + setUp(model) { + this.viewportDiv = document.createElement('div'); + + this.viewport = new tr.ui.TimelineViewport(this.viewportDiv); + + this.drawingContainer = new tr.ui.tracks.DrawingContainer(this.viewport); + this.viewport.modelTrackContainer = this.drawingContainer; + + const modelTrack = new tr.ui.tracks.ModelTrack(this.viewport); + Polymer.dom(this.drawingContainer).appendChild(modelTrack); + + modelTrack.model = model; + + Polymer.dom(this.viewportDiv).appendChild(this.drawingContainer); + + this.addHTMLOutput(this.viewportDiv); + + // Size the canvas. + this.drawingContainer.updateCanvasSizeIfNeeded_(); + + // Size the viewport. + const w = this.drawingContainer.canvas.width; + const min = model.bounds.min; + const range = model.bounds.range; + + const boost = range * 0.15; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(min - boost, min + range + boost, w); + this.viewport.setDisplayTransformImmediately(dt); + }, + + runOneIteration() { + this.drawingContainer.draw_(); + } + }; + + + function GeneralDCPerfTestCase(testName, opt_options) { + DCPerfTestCase.call(this, testName, opt_options); + } + + GeneralDCPerfTestCase.prototype = { + __proto__: DCPerfTestCase.prototype, + + setUp() { + const model = getOrCreateGeneralModel(); + DCPerfTestCase.prototype.setUp.call(this, model); + } + }; + + // Failing on Chrome canary, see + // https://github.com/catapult-project/catapult/issues/1826 + flakyTest(new GeneralDCPerfTestCase('draw_softwareCanvas_One', + {iterations: 1})); + // Failing on Chrome stable on Windows, see + // https://github.com/catapult-project/catapult/issues/1908 + flakyTest(new GeneralDCPerfTestCase('draw_softwareCanvas_Ten', + {iterations: 10})); + test(new GeneralDCPerfTestCase('draw_softwareCanvas_AHundred', + {iterations: 100})); + + function AsyncDCPerfTestCase(testName, opt_options) { + DCPerfTestCase.call(this, testName, opt_options); + } + + AsyncDCPerfTestCase.prototype = { + __proto__: DCPerfTestCase.prototype, + + setUp() { + const model = tr.c.TestUtils.newModel(function(m) { + const proc = m.getOrCreateProcess(1); + for (let tid = 1; tid <= 5; tid++) { + const thread = proc.getOrCreateThread(tid); + for (let i = 0; i < 5000; i++) { + const mod = Math.floor(i / 100) % 4; + const slice = tr.c.TestUtils.newAsyncSliceEx({ + name: 'Test' + i, + colorId: tid + mod, + id: tr.b.GUID.allocateSimple(), + start: i * 10, + duration: 9, + isTopLevel: true + }); + thread.asyncSliceGroup.push(slice); + } + } + }); + DCPerfTestCase.prototype.setUp.call(this, model); + + const w = this.drawingContainer.canvas.width; + + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(-2000, 54000, w); + this.viewport.setDisplayTransformImmediately(dt); + } + }; + test(new AsyncDCPerfTestCase('draw_asyncSliceHeavy_Twenty', + {iterations: 20})); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html new file mode 100644 index 00000000000..f8ba209d01b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * EventToTrackMap provides a mapping mechanism between events and the + * tracks those events belong on. + * @constructor + */ + function EventToTrackMap() {} + + EventToTrackMap.prototype = { + addEvent(event, track) { + if (!track) { + throw new Error('Must provide a track.'); + } + this[event.guid] = track; + } + }; + + return { + EventToTrackMap, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html new file mode 100644 index 00000000000..3e8de1d9831 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/ui/base/event_presenter.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const startCompare = function(x, y) { return x.start - y.start; }; + + /** + * Track enabling quick selection of frame slices/events. + * @constructor + */ + const FrameTrack = tr.ui.b.define( + 'frame-track', tr.ui.tracks.LetterDotTrack); + + FrameTrack.prototype = { + __proto__: tr.ui.tracks.LetterDotTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.LetterDotTrack.prototype.decorate.call(this, viewport); + this.heading = 'Frames'; + + this.frames_ = undefined; + this.items = undefined; + }, + + get frames() { + return this.frames_; + }, + + set frames(frames) { + this.frames_ = frames; + if (frames === undefined) return; + + this.frames_ = this.frames_.slice(); + this.frames_.sort(startCompare); + + // letter dots + this.items = this.frames_.map(function(frame) { + return new FrameDot(frame); + }); + } + }; + + /** + * @constructor + * @extends {LetterDot} + */ + function FrameDot(frame) { + tr.ui.tracks.LetterDot.call(this, frame, 'F', frame.colorId, frame.start); + } + + FrameDot.prototype = { + __proto__: tr.ui.tracks.LetterDot.prototype + }; + + return { + FrameTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html new file mode 100644 index 00000000000..94189d0fecb --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/frame.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> +<link rel="import" href="/tracing/ui/tracks/frame_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Frame = tr.model.Frame; + const FrameTrack = tr.ui.tracks.FrameTrack; + const EventSet = tr.model.EventSet; + const SelectionState = tr.model.SelectionState; + const Viewport = tr.ui.TimelineViewport; + + const createFrames = function() { + let frames = undefined; + const model = tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + const thread = process.getOrCreateThread(1); + for (let i = 1; i < 5; i++) { + const slice = tr.c.TestUtils.newSliceEx( + {title: 'work for frame', start: i * 20, duration: 10}); + thread.sliceGroup.pushSlice(slice); + const events = [slice]; + const threadTimeRanges = + [{thread, start: slice.start, end: slice.end}]; + process.frames.push(new Frame(events, threadTimeRanges)); + } + frames = process.frames; + }); + return frames; + }; + + test('instantiate', function() { + const frames = createFrames(); + frames[1].selectionState = SelectionState.SELECTED; + + const div = document.createElement('div'); + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = FrameTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + track.frames = frames; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 50, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + + assert.strictEqual(track.items[0].start, 20); + }); + + test('modelMapping', function() { + const frames = createFrames(); + + const div = document.createElement('div'); + const viewport = new Viewport(div); + const track = FrameTrack(viewport); + track.frames = frames; + + const a0 = track.items[0].modelItem; + assert.strictEqual(a0, frames[0]); + }); + + test('selectionMapping', function() { + const frames = createFrames(); + + const div = document.createElement('div'); + const viewport = new Viewport(div); + const track = FrameTrack(viewport); + track.frames = frames; + + const selection = new EventSet(); + track.items[0].addToSelection(selection); + + // select both frame, but not its component slice + assert.strictEqual(selection.length, 1); + + let frameCount = 0; + let eventCount = 0; + selection.forEach(function(event) { + if (event instanceof Frame) { + assert.strictEqual(event, frames[0]); + frameCount++; + } else { + eventCount++; + } + }); + assert.strictEqual(frameCount, 1); + assert.strictEqual(eventCount, 0); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html new file mode 100644 index 00000000000..aaf9bc0a80d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/tracks/chart_track.html"> +<link rel="import" href="/tracing/ui/tracks/container_track.html"> +<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html"> +<link rel="import" href="/tracing/ui/tracks/memory_dump_track_util.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const USED_MEMORY_TRACK_HEIGHT = 50; + const ALLOCATED_MEMORY_TRACK_HEIGHT = 50; + + /** + * A track that displays an array of GlobalMemoryDump objects. + * @constructor + * @extends {ContainerTrack} + */ + const GlobalMemoryDumpTrack = tr.ui.b.define( + 'global-memory-dump-track', tr.ui.tracks.ContainerTrack); + + GlobalMemoryDumpTrack.prototype = { + __proto__: tr.ui.tracks.ContainerTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport); + this.memoryDumps_ = undefined; + }, + + get memoryDumps() { + return this.memoryDumps_; + }, + + set memoryDumps(memoryDumps) { + this.memoryDumps_ = memoryDumps; + this.updateContents_(); + }, + + updateContents_() { + this.clearTracks_(); + + // Show no tracks if there are no dumps. + if (!this.memoryDumps_ || !this.memoryDumps_.length) return; + + this.appendDumpDotsTrack_(); + this.appendUsedMemoryTrack_(); + this.appendAllocatedMemoryTrack_(); + }, + + appendDumpDotsTrack_() { + const items = tr.ui.tracks.buildMemoryLetterDots(this.memoryDumps_); + if (!items) return; + + const track = new tr.ui.tracks.LetterDotTrack(this.viewport); + track.heading = 'Memory Dumps'; + track.items = items; + Polymer.dom(this).appendChild(track); + }, + + appendUsedMemoryTrack_() { + const tracks = []; + const perProcessSeries = + tr.ui.tracks.buildGlobalUsedMemoryChartSeries(this.memoryDumps_); + if (perProcessSeries !== undefined) { + tracks.push({name: 'Memory per process', series: perProcessSeries}); + } else { + tracks.push.apply(tracks, tr.ui.tracks.buildSystemMemoryChartSeries( + this.memoryDumps_[0].model)); + } + + for (const {name, series} of tracks) { + const track = new tr.ui.tracks.ChartTrack(this.viewport); + track.heading = name; + track.height = USED_MEMORY_TRACK_HEIGHT + 'px'; + track.series = series; + track.autoSetAllAxes({expandMax: true}); + Polymer.dom(this).appendChild(track); + } + }, + + appendAllocatedMemoryTrack_() { + const series = tr.ui.tracks.buildGlobalAllocatedMemoryChartSeries( + this.memoryDumps_); + if (!series) return; + + const track = new tr.ui.tracks.ChartTrack(this.viewport); + track.heading = 'Memory per component'; + track.height = ALLOCATED_MEMORY_TRACK_HEIGHT + 'px'; + track.series = series; + track.autoSetAllAxes({expandMax: true}); + Polymer.dom(this).appendChild(track); + } + }; + + return { + GlobalMemoryDumpTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html new file mode 100644 index 00000000000..20fa869bbf0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> +<link rel="import" href="/tracing/ui/tracks/global_memory_dump_track.html"> +<link rel="import" href="/tracing/ui/tracks/memory_dump_track_test_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Viewport = tr.ui.TimelineViewport; + const GlobalMemoryDumpTrack = tr.ui.tracks.GlobalMemoryDumpTrack; + const createTestGlobalMemoryDumps = tr.ui.tracks.createTestGlobalMemoryDumps; + + function instantiateTrack(withVMRegions, withAllocatorDumps, + expectedTrackCount) { + const dumps = createTestGlobalMemoryDumps( + withVMRegions, withAllocatorDumps); + + const div = document.createElement('div'); + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = new GlobalMemoryDumpTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + drawingContainer.invalidate(); + + track.memoryDumps = dumps; + this.addHTMLOutput(div); + + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 50, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + + assert.lengthOf(track.tracks_, expectedTrackCount); + } + + test('instantiate_dotsOnly', function() { + instantiateTrack.call(this, false, false, 1); + }); + + test('instantiate_withVMRegions', function() { + instantiateTrack.call(this, true, false, 2); + }); + + test('instantiate_withMemoryAllocatorDumps', function() { + instantiateTrack.call(this, false, true, 2); + }); + + test('instantiate_withBoth', function() { + instantiateTrack.call(this, true, true, 3); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html new file mode 100644 index 00000000000..7ae139672d2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/draw_helpers.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/alert_track.html"> +<link rel="import" href="/tracing/ui/tracks/container_track.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> +<link rel="import" href="/tracing/ui/tracks/kernel_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays an array of interaction records. + * @constructor + * @extends {MultiRowTrack} + */ + const InteractionTrack = tr.ui.b.define( + 'interaction-track', tr.ui.tracks.MultiRowTrack); + + InteractionTrack.prototype = { + __proto__: tr.ui.tracks.MultiRowTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport); + this.heading = 'Interactions'; + this.subRows_ = []; + }, + + set model(model) { + this.setItemsToGroup(model.userModel.expectations, { + guid: tr.b.GUID.allocateSimple(), + model, + getSettingsKey() { + return undefined; + } + }); + }, + + buildSubRows_(slices) { + if (this.subRows_.length) { + return this.subRows_; + } + this.subRows_.push( + ...tr.ui.tracks.groupAsyncSlicesIntoSubRows(slices, true)); + return this.subRows_; + }, + + addSubTrack_(slices) { + const track = new tr.ui.tracks.SliceTrack(this.viewport); + track.slices = slices; + Polymer.dom(this).appendChild(track); + return track; + } + }; + + return { + InteractionTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html new file mode 100644 index 00000000000..ac0d5692c4d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/user_model/stub_expectation.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/interaction_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + // UserExpectations should be sorted by start time, not title, so that + // AsyncSliceGroupTrack.buildSubRows_ can lay them out in as few tracks as + // possible, so that they mesh instead of stacking unnecessarily. + test('instantiate', function() { + const div = document.createElement('div'); + const viewport = new tr.ui.TimelineViewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + const track = new tr.ui.tracks.InteractionTrack(viewport); + track.model = tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + const thread = process.getOrCreateThread(1); + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + {start: 0, duration: 200})); + model.userModel.expectations.push(new tr.model.um.StubExpectation( + {parentModel: model, start: 100, duration: 100})); + model.userModel.expectations.push(new tr.model.um.StubExpectation( + {parentModel: model, start: 0, duration: 100})); + model.userModel.expectations.push(new tr.model.um.StubExpectation( + {parentModel: model, start: 150, duration: 50})); + model.userModel.expectations.push(new tr.model.um.StubExpectation( + {parentModel: model, start: 50, duration: 100})); + model.userModel.expectations.push(new tr.model.um.StubExpectation( + {parentModel: model, start: 0, duration: 50})); + // Model.createImportTracesTask() automatically sorts UEs by start time. + }); + assert.strictEqual(2, track.subRows_.length); + assert.strictEqual(2, track.subRows_[0].length); + assert.strictEqual(3, track.subRows_[1].length); + Polymer.dom(drawingContainer).appendChild(track); + this.addHTMLOutput(div); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html new file mode 100644 index 00000000000..b10547bc2e9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/tracks/cpu_track.html"> +<link rel="import" href="/tracing/ui/tracks/process_track_base.html"> +<link rel="import" href="/tracing/ui/tracks/spacing_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const Cpu = tr.model.Cpu; + const CpuTrack = tr.ui.tracks.cpu_track; + const ProcessTrackBase = tr.ui.tracks.ProcessTrackBase; + const SpacingTrack = tr.ui.tracks.SpacingTrack; + + /** + * @constructor + */ + const KernelTrack = tr.ui.b.define('kernel-track', ProcessTrackBase); + + KernelTrack.prototype = { + __proto__: ProcessTrackBase.prototype, + + decorate(viewport) { + ProcessTrackBase.prototype.decorate.call(this, viewport); + }, + + + // Kernel maps to processBase because we derive from ProcessTrackBase. + set kernel(kernel) { + this.processBase = kernel; + }, + + get kernel() { + return this.processBase; + }, + + get eventContainer() { + return this.kernel; + }, + + get hasVisibleContent() { + return this.children.length > 1; + }, + + addContainersToTrackMap(containerToTrackMap) { + tr.ui.tracks.ProcessTrackBase.prototype.addContainersToTrackMap.call( + this, containerToTrackMap); + containerToTrackMap.addContainer(this.kernel, this); + }, + + willAppendTracks_() { + const cpus = Object.values(this.kernel.cpus); + cpus.sort(tr.model.Cpu.compare); + + let didAppendAtLeastOneTrack = false; + for (let i = 0; i < cpus.length; ++i) { + const cpu = cpus[i]; + const track = new tr.ui.tracks.CpuTrack(this.viewport); + track.detailedMode = this.expanded; + track.cpu = cpu; + if (!track.hasVisibleContent) continue; + Polymer.dom(this).appendChild(track); + didAppendAtLeastOneTrack = true; + } + if (didAppendAtLeastOneTrack) { + Polymer.dom(this).appendChild(new SpacingTrack(this.viewport)); + } + } + }; + + + return { + KernelTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html new file mode 100644 index 00000000000..6a642e52ff9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html @@ -0,0 +1,251 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/proxy_selectable_item.html"> +<link rel="import" href="/tracing/ui/base/event_presenter.html"> +<link rel="import" href="/tracing/ui/base/heading.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/track.html"> + +<style> +.letter-dot-track { + height: 18px; +} +</style> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const EventPresenter = tr.ui.b.EventPresenter; + const SelectionState = tr.model.SelectionState; + + /** + * A track that displays an array of dots with filled letters inside them. + * @constructor + * @extends {Track} + */ + const LetterDotTrack = tr.ui.b.define( + 'letter-dot-track', tr.ui.tracks.Track); + + LetterDotTrack.prototype = { + __proto__: tr.ui.tracks.Track.prototype, + + decorate(viewport) { + tr.ui.tracks.Track.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('letter-dot-track'); + this.items_ = undefined; + + this.heading_ = document.createElement('tr-ui-b-heading'); + Polymer.dom(this).appendChild(this.heading_); + }, + + set heading(heading) { + this.heading_.heading = heading; + }, + + get heading() { + return this.heading_.heading; + }, + + set tooltip(tooltip) { + this.heading_.tooltip = tooltip; + }, + + get items() { + return this.items_; + }, + + set items(items) { + this.items_ = items; + this.invalidateDrawingContainer(); + }, + + get height() { + return window.getComputedStyle(this).height; + }, + + set height(height) { + this.style.height = height; + }, + + get dumpRadiusView() { + return 7 * (window.devicePixelRatio || 1); + }, + + draw(type, viewLWorld, viewRWorld, viewHeight) { + if (this.items_ === undefined) return; + + switch (type) { + case tr.ui.tracks.DrawType.GENERAL_EVENT: + this.drawLetterDots_(viewLWorld, viewRWorld); + break; + } + }, + + drawLetterDots_(viewLWorld, viewRWorld) { + const ctx = this.context(); + const pixelRatio = window.devicePixelRatio || 1; + + const bounds = this.getBoundingClientRect(); + const height = bounds.height * pixelRatio; + const halfHeight = height * 0.5; + const twoPi = Math.PI * 2; + + // Culling parameters. + const dt = this.viewport.currentDisplayTransform; + const dumpRadiusView = this.dumpRadiusView; + const itemRadiusWorld = dt.xViewVectorToWorld(height); + + // Draw the memory dumps. + const items = this.items_; + const loI = tr.b.findLowIndexInSortedArray( + items, + function(item) { return item.start; }, + viewLWorld); + + const oldFont = ctx.font; + ctx.font = '400 ' + Math.floor(9 * pixelRatio) + 'px Arial'; + ctx.strokeStyle = 'rgb(0,0,0)'; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + + const drawItems = function(selected) { + for (let i = loI; i < items.length; ++i) { + const item = items[i]; + const x = item.start; + if (x - itemRadiusWorld > viewRWorld) break; + + if (item.selected !== selected) continue; + + const xView = dt.xWorldToView(x); + + ctx.fillStyle = EventPresenter.getSelectableItemColorAsString(item); + ctx.beginPath(); + ctx.arc(xView, halfHeight, dumpRadiusView + 0.5, 0, twoPi); + ctx.fill(); + if (item.selected) { + ctx.lineWidth = 3; + ctx.strokeStyle = 'rgb(100,100,0)'; + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(xView, halfHeight, dumpRadiusView, 0, twoPi); + ctx.lineWidth = 1.5; + ctx.strokeStyle = 'rgb(255,255,0)'; + ctx.stroke(); + } else { + ctx.lineWidth = 1; + ctx.strokeStyle = 'rgb(0,0,0)'; + ctx.stroke(); + } + + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillText(item.dotLetter, xView, halfHeight); + } + }; + + // Draw unselected items first to make sure they don't occlude selected + // items. + drawItems(false); + drawItems(true); + + ctx.lineWidth = 1; + ctx.font = oldFont; + }, + + addEventsToTrackMap(eventToTrackMap) { + if (this.items_ === undefined) return; + + this.items_.forEach(function(item) { + item.addToTrackMap(eventToTrackMap, this); + }, this); + }, + + addIntersectingEventsInRangeToSelectionInWorldSpace( + loWX, hiWX, viewPixWidthWorld, selection) { + if (this.items_ === undefined) return; + + const itemRadiusWorld = viewPixWidthWorld * this.dumpRadiusView; + tr.b.iterateOverIntersectingIntervals( + this.items_, + function(x) { return x.start - itemRadiusWorld; }, + function(x) { return 2 * itemRadiusWorld; }, + loWX, hiWX, + function(item) { + item.addToSelection(selection); + }.bind(this)); + }, + + /** + * Add the item to the left or right of the provided event, if any, to the + * selection. + * @param {event} The current event item. + * @param {Number} offset Number of slices away from the event to look. + * @param {Selection} selection The selection to add an event to, + * if found. + * @return {boolean} Whether an event was found. + * @private + */ + addEventNearToProvidedEventToSelection(event, offset, selection) { + if (this.items_ === undefined) return; + + const index = this.items_.findIndex(item => item.modelItem === event); + if (index === -1) return false; + + const newIndex = index + offset; + if (newIndex >= 0 && newIndex < this.items_.length) { + this.items_[newIndex].addToSelection(selection); + return true; + } + return false; + }, + + addAllEventsMatchingFilterToSelection(filter, selection) { + }, + + addClosestEventToSelection(worldX, worldMaxDist, loY, hiY, + selection) { + if (this.items_ === undefined) return; + + const item = tr.b.findClosestElementInSortedArray( + this.items_, + function(x) { return x.start; }, + worldX, + worldMaxDist); + + if (!item) return; + + item.addToSelection(selection); + } + }; + + /** + * A filled dot with a letter inside it. + * + * @constructor + * @extends {ProxySelectableItem} + */ + function LetterDot(modelItem, dotLetter, colorId, start) { + tr.model.ProxySelectableItem.call(this, modelItem); + this.dotLetter = dotLetter; + this.colorId = colorId; + this.start = start; + } + + LetterDot.prototype = { + __proto__: tr.model.ProxySelectableItem.prototype + }; + + return { + LetterDotTrack, + LetterDot, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html new file mode 100644 index 00000000000..b37034afab2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html @@ -0,0 +1,121 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/selection_state.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> +<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const LetterDotTrack = tr.ui.tracks.LetterDotTrack; + const LetterDot = tr.ui.tracks.LetterDot; + const SelectionState = tr.model.SelectionState; + const Viewport = tr.ui.TimelineViewport; + + const createItems = function() { + const items = [ + new LetterDot({selectionState: SelectionState.SELECTED}, 'a', 7, 5), + new LetterDot({selectionState: SelectionState.SELECTED}, 'b', 2, 20), + new LetterDot({selectionState: SelectionState.NONE}, 'c', 4, 35), + new LetterDot({selectionState: SelectionState.NONE}, 'd', 4, 50) + ]; + return items; + }; + + test('instantiate', function() { + const items = createItems(); + + const div = document.createElement('div'); + + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = LetterDotTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + track.items = items; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 60, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + }); + + test('selectionHitTesting', function() { + const items = createItems(); + + const track = new LetterDotTrack(new Viewport()); + track.items = items; + + // Fake a view pixel size. + const devicePixelRatio = window.devicePixelRatio || 1; + const viewPixWidthWorld = 0.1 / devicePixelRatio; + + // Hit outside range + let selection = []; + track.addIntersectingEventsInRangeToSelectionInWorldSpace( + 3, 4, viewPixWidthWorld, selection); + assert.strictEqual(selection.length, 0); + + // Hit the first item, via pixel-nearness. + selection = []; + track.addIntersectingEventsInRangeToSelectionInWorldSpace( + 19.98, 19.99, viewPixWidthWorld, selection); + assert.strictEqual(selection.length, 1); + assert.strictEqual(selection[0], items[1].modelItem); + + // Hit the instance, between the 1st and 2nd snapshots + selection = []; + track.addIntersectingEventsInRangeToSelectionInWorldSpace( + 30, 50, viewPixWidthWorld, selection); + assert.strictEqual(selection.length, 2); + assert.strictEqual(selection[0], items[2].modelItem); + assert.strictEqual(selection[1], items[3].modelItem); + }); + + test('addEventNearToProvidedEventToSelection', function() { + const items = createItems(); + + const track = new LetterDotTrack(new Viewport()); + track.items = items; + + // Right from the middle of items. + const selection1 = []; + assert.isTrue(track.addEventNearToProvidedEventToSelection( + items[2].modelItem, 1, selection1)); + assert.strictEqual(selection1.length, 1); + assert.strictEqual(selection1[0], items[3].modelItem); + + // Left from the middle of items. + const selection2 = []; + assert.isTrue(track.addEventNearToProvidedEventToSelection( + items[2].modelItem, -1, selection2)); + assert.strictEqual(selection2.length, 1); + assert.strictEqual(selection2[0], items[1].modelItem); + + // Right from the right edge of items. + const selection3 = []; + assert.isFalse(track.addEventNearToProvidedEventToSelection( + items[3].modelItem, 1, selection3)); + assert.strictEqual(selection3.length, 0); + + // Left from the left edge of items. + const selection4 = []; + assert.isFalse(track.addEventNearToProvidedEventToSelection( + items[0].modelItem, -1, selection4)); + assert.strictEqual(selection4.length, 0); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html new file mode 100644 index 00000000000..611bd8664f8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html @@ -0,0 +1,155 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/container_memory_dump.html"> +<link rel="import" href="/tracing/model/global_memory_dump.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/process_memory_dump.html"> +<link rel="import" href="/tracing/model/selection_state.html"> +<link rel="import" href="/tracing/model/vm_region.html"> + +<script> +'use strict'; + +/** + * @fileoverview Helper functions for memory dump track tests. + */ +tr.exportTo('tr.ui.tracks', function() { + const ProcessMemoryDump = tr.model.ProcessMemoryDump; + const GlobalMemoryDump = tr.model.GlobalMemoryDump; + const VMRegion = tr.model.VMRegion; + const VMRegionClassificationNode = tr.model.VMRegionClassificationNode; + const SelectionState = tr.model.SelectionState; + const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump; + const addProcessMemoryDump = + tr.model.MemoryDumpTestUtils.addProcessMemoryDump; + const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump; + const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink; + const BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND; + const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT; + const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED; + + function createVMRegions(pssValues) { + return VMRegionClassificationNode.fromRegions( + pssValues.map(function(pssValue, i) { + return VMRegion.fromDict({ + startAddress: 1000 * i, + sizeInBytes: 1000, + protectionFlags: VMRegion.PROTECTION_FLAG_READ, + mappedFile: '[stack' + i + ']', + byteStats: { + privateDirtyResident: pssValue / 3, + swapped: pssValue * 3, + proportionalResident: pssValue + } + }); + })); + } + + function createAllocatorDumps(memoryDump, dumpData) { + // Create the individual allocator dumps. + const allocatorDumps = {}; + for (const [allocatorName, data] of Object.entries(dumpData)) { + const size = data.size; + assert.typeOf(size, 'number'); // Sanity check. + allocatorDumps[allocatorName] = newAllocatorDump( + memoryDump, allocatorName, {numerics: {size}}); + } + + // Add ownership links between them. + for (const [allocatorName, data] of Object.entries(dumpData)) { + const owns = data.owns; + if (owns === undefined) continue; + + const ownerDump = allocatorDumps[allocatorName]; + assert.isDefined(ownerDump); // Sanity check. + const ownedDump = allocatorDumps[owns]; + assert.isDefined(ownedDump); // Sanity check. + + addOwnershipLink(ownerDump, ownedDump); + } + + return Object.values(allocatorDumps); + } + + function addProcessMemoryDumpWithFields(globalMemoryDump, process, start, + opt_pssValues, opt_dumpData) { + const pmd = addProcessMemoryDump(globalMemoryDump, process, {ts: start}); + if (opt_pssValues !== undefined) { + pmd.vmRegions = createVMRegions(opt_pssValues); + } + if (opt_dumpData !== undefined) { + pmd.memoryAllocatorDumps = createAllocatorDumps(pmd, opt_dumpData); + } + } + + function createModelWithDumps(withVMRegions, withAllocatorDumps) { + const maybePssValues = function(pssValues) { + return withVMRegions ? pssValues : undefined; + }; + const maybeDumpData = function(dumpData) { + return withAllocatorDumps ? dumpData : undefined; + }; + return tr.c.TestUtils.newModel(function(model) { + // Construct a model with three processes. + const pa = model.getOrCreateProcess(3); + const pb = model.getOrCreateProcess(6); + const pc = model.getOrCreateProcess(9); + + const gmd1 = addGlobalMemoryDump(model, {ts: 0, levelOfDetail: LIGHT}); + addProcessMemoryDumpWithFields(gmd1, pa, 0, maybePssValues([111])); + addProcessMemoryDumpWithFields(gmd1, pb, 0.2, undefined, + maybeDumpData({oilpan: {size: 1024}})); + + const gmd2 = addGlobalMemoryDump(model, {ts: 5, levelOfDetail: DETAILED}); + addProcessMemoryDumpWithFields(gmd2, pa, 0); + addProcessMemoryDumpWithFields(gmd2, pb, 4.99, maybePssValues([100, 50]), + maybeDumpData({v8: {size: 512}})); + addProcessMemoryDumpWithFields(gmd2, pc, 5.12, undefined, + maybeDumpData({oilpan: {size: 128, owns: 'v8'}, + v8: {size: 384, owns: 'tracing'}, tracing: {size: 65920}})); + + const gmd3 = addGlobalMemoryDump( + model, {ts: 15, levelOfDetail: DETAILED}); + addProcessMemoryDumpWithFields(gmd3, pa, 15.5, maybePssValues([]), + maybeDumpData({v8: {size: 768}})); + addProcessMemoryDumpWithFields(gmd3, pc, 14.5, + maybePssValues([70, 70, 70]), maybeDumpData({oilpan: {size: 512}})); + + const gmd4 = addGlobalMemoryDump(model, {ts: 18, levelOfDetail: LIGHT}); + + const gmd5 = addGlobalMemoryDump(model, + {ts: 20, levelOfDetail: BACKGROUND}); + addProcessMemoryDumpWithFields(gmd5, pa, 0, maybePssValues([105])); + addProcessMemoryDumpWithFields(gmd5, pb, 0.2, undefined, + maybeDumpData({oilpan: {size: 100}})); + }); + } + + function createTestGlobalMemoryDumps(withVMRegions, withAllocatorDumps) { + const model = createModelWithDumps(withVMRegions, withAllocatorDumps); + const dumps = model.globalMemoryDumps; + dumps[1].selectionState = SelectionState.HIGHLIGHTED; + dumps[2].selectionState = SelectionState.SELECTED; + return dumps; + } + + function createTestProcessMemoryDumps(withVMRegions, withAllocatorDumps) { + const model = createModelWithDumps(withVMRegions, withAllocatorDumps); + const dumps = model.getProcess(9).memoryDumps; + dumps[0].selectionState = SelectionState.SELECTED; + dumps[1].selectionState = SelectionState.HIGHLIGHTED; + return dumps; + } + + return { + createTestGlobalMemoryDumps, + createTestProcessMemoryDumps, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html new file mode 100644 index 00000000000..a483d545880 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html @@ -0,0 +1,253 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/container_memory_dump.html"> +<link rel="import" href="/tracing/model/memory_allocator_dump.html"> +<link rel="import" href="/tracing/ui/tracks/chart_point.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html"> +<link rel="import" href="/tracing/ui/tracks/chart_track.html"> +<link rel="import" href="/tracing/ui/tracks/container_track.html"> +<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const ColorScheme = tr.b.ColorScheme; + + const DISPLAYED_SIZE_NUMERIC_NAME = + tr.model.MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME; + const BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND; + const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT; + const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED; + + const SYSTEM_MEMORY_CHART_RENDERING_CONFIG = { + chartType: tr.ui.tracks.ChartSeriesType.AREA, + colorId: ColorScheme.getColorIdForGeneralPurposeString('systemMemory'), + backgroundOpacity: 0.8 + }; + const SYSTEM_MEMORY_SERIES_NAMES = ['Used (KB)', 'Swapped (KB)']; + + /** Extract PSS values of processes in a global memory dump. */ + function extractGlobalMemoryDumpUsedSizes(globalMemoryDump, addSize) { + for (const [pid, pmd] of + Object.entries(globalMemoryDump.processMemoryDumps)) { + const mostRecentVmRegions = pmd.mostRecentVmRegions; + if (mostRecentVmRegions === undefined) continue; + addSize(pid, mostRecentVmRegions.byteStats.proportionalResident || 0, + pmd.process.userFriendlyName); + } + } + + /** Extract sizes of root allocators in a process memory dump. */ + function extractProcessMemoryDumpAllocatorSizes(processMemoryDump, addSize) { + const allocatorDumps = processMemoryDump.memoryAllocatorDumps; + if (allocatorDumps === undefined) return; + + allocatorDumps.forEach(function(allocatorDump) { + // Don't show tracing overhead in the charts. + // TODO(petrcermak): Find a less hacky way to do this. + if (allocatorDump.fullName === 'tracing') return; + + const allocatorSize = allocatorDump.numerics[DISPLAYED_SIZE_NUMERIC_NAME]; + if (allocatorSize === undefined) return; + + const allocatorSizeValue = allocatorSize.value; + if (allocatorSizeValue === undefined) return; + + addSize(allocatorDump.fullName, allocatorSizeValue); + }); + } + + /** Extract sizes of root allocators in a global memory dump. */ + function extractGlobalMemoryDumpAllocatorSizes(globalMemoryDump, addSize) { + for (const pmd of Object.values(globalMemoryDump.processMemoryDumps)) { + extractProcessMemoryDumpAllocatorSizes(pmd, addSize); + } + } + + /** + * A generic function which converts a list of memory dumps to a list of + * chart series. + * + * @param {!Array<!tr.model.ContainerMemoryDump>} memoryDumps List of + * container memory dumps. + * @param {!function( + * !tr.model.ContainerMemoryDump, + * !function(string, number, string=))} dumpSizeExtractor Callback for + * extracting sizes from a container memory dump. + * @return {(!Array<!tr.ui.tracks.ChartSeries>|undefined)} List of chart + * series (or undefined if no size is extracted from any container memory + * dump). + */ + function buildMemoryChartSeries(memoryDumps, dumpSizeExtractor) { + const dumpCount = memoryDumps.length; + const idToTimestampToPoint = {}; + const idToName = {}; + + // Extract the sizes of all components from each memory dump. + memoryDumps.forEach(function(dump, index) { + dumpSizeExtractor(dump, function addSize(id, size, opt_name) { + let timestampToPoint = idToTimestampToPoint[id]; + if (timestampToPoint === undefined) { + idToTimestampToPoint[id] = timestampToPoint = new Array(dumpCount); + for (let i = 0; i < dumpCount; i++) { + const modelItem = memoryDumps[i]; + timestampToPoint[i] = new tr.ui.tracks.ChartPoint( + modelItem, modelItem.start, 0); + } + } + timestampToPoint[index].y += size; + if (opt_name !== undefined) idToName[id] = opt_name; + }); + }); + + // Do not generate any chart series if no sizes were extracted. + const ids = Object.keys(idToTimestampToPoint); + if (ids.length === 0) return undefined; + + ids.sort(); + for (let i = 0; i < dumpCount; i++) { + let baseSize = 0; + // Traverse |ids| in reverse (alphabetical) order so that the first id is + // at the top of the chart. + for (let j = ids.length - 1; j >= 0; j--) { + const point = idToTimestampToPoint[ids[j]][i]; + point.yBase = baseSize; + point.y += baseSize; + baseSize = point.y; + } + } + + // Create one common axis for all memory chart series. + const seriesYAxis = new tr.ui.tracks.ChartSeriesYAxis(0); + + // Build a chart series for each id. + const series = ids.map(function(id) { + const colorId = ColorScheme.getColorIdForGeneralPurposeString( + idToName[id] || id); + const renderingConfig = { + chartType: tr.ui.tracks.ChartSeriesType.AREA, + colorId, + backgroundOpacity: 0.8 + }; + return new tr.ui.tracks.ChartSeries(idToTimestampToPoint[id], + seriesYAxis, renderingConfig); + }); + + // Ensure that the series at the top of the chart are drawn last. + series.reverse(); + + return series; + } + + /** + * Transform a list of memory dumps to a list of letter dots (with letter 'M' + * inside). + */ + function buildMemoryLetterDots(memoryDumps) { + const backgroundMemoryColorId = + ColorScheme.getColorIdForReservedName('background_memory_dump'); + const lightMemoryColorId = + ColorScheme.getColorIdForReservedName('light_memory_dump'); + const detailedMemoryColorId = + ColorScheme.getColorIdForReservedName('detailed_memory_dump'); + return memoryDumps.map(function(memoryDump) { + let memoryColorId; + switch (memoryDump.levelOfDetail) { + case BACKGROUND: + memoryColorId = backgroundMemoryColorId; + break; + case DETAILED: + memoryColorId = detailedMemoryColorId; + break; + case LIGHT: + default: + memoryColorId = lightMemoryColorId; + } + return new tr.ui.tracks.LetterDot( + memoryDump, 'M', memoryColorId, memoryDump.start); + }); + } + + /** + * Convert a list of global memory dumps to a list of chart series (one per + * process). Each series represents the evolution of the memory used by the + * process over time. + */ + function buildGlobalUsedMemoryChartSeries(globalMemoryDumps) { + return buildMemoryChartSeries(globalMemoryDumps, + extractGlobalMemoryDumpUsedSizes); + } + + /** + * Convert a list of process memory dumps to a list of chart series (one per + * root allocator). Each series represents the evolution of the size of a the + * corresponding root allocator (e.g. 'v8') over time. + */ + function buildProcessAllocatedMemoryChartSeries(processMemoryDumps) { + return buildMemoryChartSeries(processMemoryDumps, + extractProcessMemoryDumpAllocatorSizes); + } + + /** + * Convert a list of global memory dumps to a list of chart series (one per + * root allocator). Each series represents the evolution of the size of a the + * corresponding root allocator (e.g. 'v8') over time. + */ + function buildGlobalAllocatedMemoryChartSeries(globalMemoryDumps) { + return buildMemoryChartSeries(globalMemoryDumps, + extractGlobalMemoryDumpAllocatorSizes); + } + + /** + * Converts system memory counters in the model to a list of + * {'name': trackName, 'series': ChartSeries}. + */ + function buildSystemMemoryChartSeries(model) { + if (model.kernel.counters === undefined) return; + const memoryCounter = model.kernel.counters['global.SystemMemory']; + if (memoryCounter === undefined) return; + + const tracks = []; + for (const name of SYSTEM_MEMORY_SERIES_NAMES) { + const series = memoryCounter.series.find(series => series.name === name); + if (series === undefined || series.samples.length === 0) return; + + const chartPoints = []; + const valueRange = new tr.b.math.Range(); + for (const sample of series.samples) { + chartPoints.push(new tr.ui.tracks.ChartPoint( + sample, sample.timestamp, sample.value, 0)); + valueRange.addValue(sample.value); + } + // Stretch min to max range over the top half of a chart for readability. + const baseLine = Math.max(0, valueRange.min - valueRange.range); + const axisY = new tr.ui.tracks.ChartSeriesYAxis(baseLine, valueRange.max); + const chartSeries = + [new tr.ui.tracks.ChartSeries(chartPoints, axisY, + SYSTEM_MEMORY_CHART_RENDERING_CONFIG)]; + tracks.push({ + name: 'System Memory ' + name, + series: chartSeries + }); + } + return tracks; + } + + return { + buildMemoryLetterDots, + buildGlobalUsedMemoryChartSeries, + buildProcessAllocatedMemoryChartSeries, + buildGlobalAllocatedMemoryChartSeries, + buildSystemMemoryChartSeries, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html new file mode 100644 index 00000000000..f4f9451b4b9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html @@ -0,0 +1,270 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/selection_state.html"> +<link rel="import" href="/tracing/ui/tracks/memory_dump_track_test_utils.html"> +<link rel="import" href="/tracing/ui/tracks/memory_dump_track_util.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const SelectionState = tr.model.SelectionState; + const createTestGlobalMemoryDumps = tr.ui.tracks.createTestGlobalMemoryDumps; + const createTestProcessMemoryDumps = + tr.ui.tracks.createTestProcessMemoryDumps; + + test('buildMemoryLetterDots_withoutVMRegions', function() { + const dumps = createTestGlobalMemoryDumps(false, false); + const items = tr.ui.tracks.buildMemoryLetterDots(dumps); + + assert.lengthOf(items, 5); + assert.strictEqual(items[0].start, 0); + assert.strictEqual(items[1].start, 5); + assert.strictEqual(items[2].start, 15); + assert.strictEqual(items[3].start, 18); + assert.strictEqual(items[4].start, 20); + + // Check model mapping. + assert.strictEqual(items[1].selectionState, SelectionState.HIGHLIGHTED); + assert.isTrue(items[2].selected); + assert.strictEqual(items[3].modelItem, dumps[3]); + }); + + test('buildMemoryLetterDots_withVMRegions', function() { + const dumps = createTestGlobalMemoryDumps(false, false); + const items = tr.ui.tracks.buildMemoryLetterDots(dumps); + + assert.lengthOf(items, 5); + assert.strictEqual(items[0].start, 0); + assert.strictEqual(items[1].start, 5); + assert.strictEqual(items[2].start, 15); + assert.strictEqual(items[3].start, 18); + assert.strictEqual(items[4].start, 20); + + // Check model mapping. + assert.strictEqual(items[1].selectionState, SelectionState.HIGHLIGHTED); + assert.isTrue(items[2].selected); + assert.strictEqual(items[3].modelItem, dumps[3]); + }); + + test('buildGlobalUsedMemoryChartSeries_withoutVMRegions', function() { + const dumps = createTestGlobalMemoryDumps(false, false); + const series = tr.ui.tracks.buildGlobalUsedMemoryChartSeries(dumps); + + assert.isUndefined(series); + }); + + test('buildGlobalUsedMemoryChartSeries_withVMRegions', function() { + const dumps = createTestGlobalMemoryDumps(true, false); + const series = tr.ui.tracks.buildGlobalUsedMemoryChartSeries(dumps); + + assert.lengthOf(series, 3); + + const sa = series[2]; + const sb = series[1]; + const sc = series[0]; + + assert.lengthOf(sa.points, 5); + assert.lengthOf(sb.points, 5); + assert.lengthOf(sc.points, 5); + + // Process A: VM regions defined -> sum their PSS values (111). + // Process B: VM regions undefined and no previous value -> assume zero. + // Process C: Memory dump not present -> assume process not alive (0). + assert.strictEqual(sa.points[0].x, 0); + assert.strictEqual(sb.points[0].x, 0); + assert.strictEqual(sc.points[0].x, 0); + assert.strictEqual(sa.points[0].y, 111); + assert.strictEqual(sb.points[0].y, 0); + assert.strictEqual(sc.points[0].y, 0); + assert.strictEqual(sa.points[0].yBase, 0); + assert.strictEqual(sb.points[0].yBase, 0); + assert.strictEqual(sc.points[0].yBase, 0); + + // Process A: VM regions undefined -> assume previous value (111). + // Process B: VM regions defined -> sum their PSS values (100 + 50). + // Process C: VM regions undefined -> assume previous value (0). + assert.strictEqual(sa.points[1].x, 5); + assert.strictEqual(sb.points[1].x, 5); + assert.strictEqual(sc.points[1].x, 5); + assert.strictEqual(sa.points[1].y, 150 + 111); + assert.strictEqual(sb.points[1].y, 150); + assert.strictEqual(sc.points[1].y, 0); + assert.strictEqual(sa.points[1].yBase, 150); + assert.strictEqual(sb.points[1].yBase, 0); + assert.strictEqual(sc.points[1].yBase, 0); + + // Process A: VM regions defined -> sum their PSS values (0). + // Process B: Memory dump not present -> assume process not alive (0). + // Process C: VM regions defined -> sum their PSS values (70 + 70 + 70). + assert.strictEqual(sa.points[2].x, 15); + assert.strictEqual(sb.points[2].x, 15); + assert.strictEqual(sc.points[2].x, 15); + assert.strictEqual(sa.points[2].y, 210); + assert.strictEqual(sb.points[2].y, 210); + assert.strictEqual(sc.points[2].y, 210); + assert.strictEqual(sa.points[2].yBase, 210); + assert.strictEqual(sb.points[2].yBase, 210); + assert.strictEqual(sc.points[2].yBase, 0); + + // All processes: Memory dump not present -> assume process not alive (0). + assert.strictEqual(sa.points[3].x, 18); + assert.strictEqual(sb.points[3].x, 18); + assert.strictEqual(sc.points[3].x, 18); + assert.strictEqual(sa.points[3].y, 0); + assert.strictEqual(sb.points[3].y, 0); + assert.strictEqual(sc.points[3].y, 0); + assert.strictEqual(sa.points[3].yBase, 0); + assert.strictEqual(sb.points[3].yBase, 0); + assert.strictEqual(sc.points[3].yBase, 0); + + // Process A: VM regions defined -> sum their PSS values (105). + // Process B: VM regions undefined and no previous value -> assume zero. + // Process C: Memory dump not present -> assume process not alive (0). + assert.strictEqual(sa.points[4].x, 20); + assert.strictEqual(sb.points[4].x, 20); + assert.strictEqual(sc.points[4].x, 20); + assert.strictEqual(sa.points[4].y, 105); + assert.strictEqual(sb.points[4].y, 0); + assert.strictEqual(sc.points[4].y, 0); + assert.strictEqual(sa.points[4].yBase, 0); + assert.strictEqual(sb.points[4].yBase, 0); + assert.strictEqual(sc.points[4].yBase, 0); + + // Check model mapping. + assert.strictEqual(sa.points[1].selectionState, SelectionState.HIGHLIGHTED); + assert.isTrue(sb.points[2].selected); + assert.strictEqual(sc.points[3].modelItem, dumps[3]); + }); + + test('buildGlobalAllocatedMemoryChartSeries_withoutMemoryAllocatorDumps', + function() { + const dumps = createTestGlobalMemoryDumps(false, false); + const series = tr.ui.tracks.buildGlobalAllocatedMemoryChartSeries( + dumps); + + assert.isUndefined(series); + }); + + test('buildGlobalAllocatedMemoryChartSeries_withMemoryAllocatorDumps', + function() { + const dumps = createTestGlobalMemoryDumps(false, true); + const series = tr.ui.tracks.buildGlobalAllocatedMemoryChartSeries( + dumps); + + assert.lengthOf(series, 2); + + const so = series[1]; + const sv = series[0]; + + assert.lengthOf(so.points, 5); + assert.lengthOf(sv.points, 5); + + // Oilpan: Only process B dumps allocated objects size (1024). + // V8: No process dumps allocated objects size (0). + assert.strictEqual(so.points[0].x, 0); + assert.strictEqual(sv.points[0].x, 0); + assert.strictEqual(so.points[0].y, 1024); + assert.strictEqual(sv.points[0].y, 0); + assert.strictEqual(so.points[0].yBase, 0); + assert.strictEqual(sv.points[0].yBase, 0); + + // Oilpan: Process B did not provide a value and process C dumps (128). + // V8: Processes B and C dump (512 + 256). + assert.strictEqual(so.points[1].x, 5); + assert.strictEqual(sv.points[1].x, 5); + assert.strictEqual(so.points[1].y, 768 + 128); + assert.strictEqual(sv.points[1].y, 768); + assert.strictEqual(so.points[1].yBase, 768); + assert.strictEqual(sv.points[1].yBase, 0); + + // Oilpan: Process B assumed not alive and process C dumps (512) + // V8: Process A dumps now, process B assumed not alive, process C did + // not provide a value (768). + assert.strictEqual(so.points[2].x, 15); + assert.strictEqual(sv.points[2].x, 15); + assert.strictEqual(so.points[2].y, 768 + 512); + assert.strictEqual(sv.points[2].y, 768); + assert.strictEqual(so.points[2].yBase, 768); + assert.strictEqual(sv.points[2].yBase, 0); + + // All processes: Memory dump not present -> assume process not alive + // (0). + assert.strictEqual(so.points[3].x, 18); + assert.strictEqual(sv.points[3].x, 18); + assert.strictEqual(so.points[3].y, 0); + assert.strictEqual(sv.points[3].y, 0); + assert.strictEqual(so.points[3].yBase, 0); + assert.strictEqual(sv.points[3].yBase, 0); + + // Oilpan: Only process B dumps allocated objects size (100). + // V8: No process dumps allocated objects size (0). + assert.strictEqual(so.points[4].x, 20); + assert.strictEqual(sv.points[4].x, 20); + assert.strictEqual(so.points[4].y, 100); + assert.strictEqual(sv.points[4].y, 0); + + // Check model mapping. + assert.strictEqual( + so.points[1].selectionState, SelectionState.HIGHLIGHTED); + assert.isTrue(sv.points[2].selected); + assert.strictEqual(so.points[3].modelItem, dumps[3]); + }); + + test('buildProcessAllocatedMemoryChartSeries_withoutMemoryAllocatorDumps', + function() { + const dumps = createTestProcessMemoryDumps(false, false); + const series = tr.ui.tracks.buildProcessAllocatedMemoryChartSeries( + dumps); + + assert.isUndefined(series); + }); + + test('buildProcessAllocatedMemoryChartSeries_withMemoryAllocatorDumps', + function() { + const dumps = createTestProcessMemoryDumps(false, true); + const series = tr.ui.tracks.buildProcessAllocatedMemoryChartSeries( + dumps); + + // There should be only 2 series (because 'tracing' is not shown in the + // charts). + assert.lengthOf(series, 2); + + const so = series[1]; + const sv = series[0]; + + assert.lengthOf(so.points, 2); + assert.lengthOf(sv.points, 2); + + // Oilpan: Process dumps (128). + // V8: Process dumps (256). + assert.strictEqual(so.points[0].x, 5.12); + assert.strictEqual(sv.points[0].x, 5.12); + assert.strictEqual(so.points[0].y, 256 + 128); + assert.strictEqual(sv.points[0].y, 256); + assert.strictEqual(so.points[0].yBase, 256); + assert.strictEqual(sv.points[0].yBase, 0); + + // Oilpan: Process dumps (512). + // V8: Process did not provide a value (0). + assert.strictEqual(so.points[1].x, 14.5); + assert.strictEqual(sv.points[1].x, 14.5); + assert.strictEqual(so.points[1].y, 512); + assert.strictEqual(sv.points[1].y, 0); + assert.strictEqual(so.points[1].yBase, 0); + assert.strictEqual(sv.points[1].yBase, 0); + + // Check model mapping. + assert.strictEqual( + so.points[1].selectionState, SelectionState.HIGHLIGHTED); + assert.isTrue(sv.points[0].selected); + assert.strictEqual(so.points[1].modelItem, dumps[1]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html new file mode 100644 index 00000000000..bf59f11349e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const ColorScheme = tr.b.ColorScheme; + const LetterDotTrack = tr.ui.tracks.LetterDotTrack; + + /** + * A track that displays global memory events. + * + * @constructor + * @extends {tr.ui.tracks.LetterDotTrack} + */ + const MemoryTrack = tr.ui.b.define('memory-track', LetterDotTrack); + + MemoryTrack.prototype = { + __proto__: LetterDotTrack.prototype, + + decorate(viewport) { + LetterDotTrack.prototype.decorate.call(this, viewport); + this.classList.add('memory-track'); + this.heading = 'Memory Events'; + this.lowMemoryEvents_ = undefined; + }, + + initialize(model) { + if (model !== undefined) { + this.lowMemoryEvents_ = model.device.lowMemoryEvents; + } else { + this.lowMemoryEvents_ = undefined; + } + + if (this.hasVisibleContent) { + this.items = this.buildMemoryLetterDots_(this.lowMemoryEvents_); + } + }, + + get hasVisibleContent() { + return !!this.lowMemoryEvents_ && this.lowMemoryEvents_.length !== 0; + }, + + buildMemoryLetterDots_(memoryEvents) { + return memoryEvents.map( + memoryEvent => new tr.ui.tracks.LetterDot( + memoryEvent, + 'K', + ColorScheme.getColorIdForReservedName('background_memory_dump'), + memoryEvent.start + ) + ); + }, + }; + + return { + MemoryTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html new file mode 100644 index 00000000000..60f1c8ccd2c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html @@ -0,0 +1,99 @@ +<!DOCTYPE html> +<!-- + Copyright 2017 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/memory/lowmemory_auditor.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/thread_slice.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> +<link rel="import" href="/tracing/ui/tracks/memory_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const ThreadSlice = tr.model.ThreadSlice; + + // Input : slices is an array-of-array-of slices. Each top level array + // represents a process. So, each slice in one of the top level array + // will be placed in the same process. + function buildModel(slices) { + const model = tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + for (let i = 0; i < slices.length; i++) { + const thread = process.getOrCreateThread(i); + slices[i].forEach(s => thread.sliceGroup.pushSlice(s)); + } + }); + + const auditor = new tr.e.audits.LowMemoryAuditor(model); + auditor.runAnnotate(); + return model; + } + + function createMemoryTrack(model, interval) { + const div = document.createElement('div'); + const viewport = new tr.ui.TimelineViewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + div.appendChild(drawingContainer); + const track = new tr.ui.tracks.MemoryTrack(drawingContainer.viewport); + if (model !== undefined) { + setDisplayTransformFromBounds(viewport, model.bounds); + } + track.initialize(model, interval); + drawingContainer.appendChild(track); + this.addHTMLOutput(drawingContainer); + return track; + } + + /** + * Sets the mapping between the input range of timestamps and the output range + * of horizontal pixels. + */ + function setDisplayTransformFromBounds(viewport, bounds) { + const dt = new tr.ui.TimelineDisplayTransform(); + const pixelRatio = window.devicePixelRatio || 1; + const chartPixelWidth = + (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio; + dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth); + viewport.setDisplayTransformImmediately(dt); + } + + test('instantiate', function() { + const sliceA = new tr.model.ThreadSlice('lowmemory', title, 0, 5.5111, {}); + const sliceB = new tr.model.ThreadSlice('lowmemory', title, 0, 11.2384, {}); + + const model = buildModel([[sliceA, sliceB]]); + createMemoryTrack.call(this, model); + }); + + test('hasVisibleContent_trueWithThreadSlicePresent', function() { + const sliceA = new tr.model.ThreadSlice('lowmemory', title, 0, 5.5111, {}); + const sliceB = new tr.model.ThreadSlice('lowmemory', title, 0, 11.2384, {}); + const model = buildModel([[sliceA, sliceB]]); + const track = createMemoryTrack.call(this, model); + + assert.isTrue(track.hasVisibleContent); + }); + + test('hasVisibleContent_falseWithUndefinedProcessModel', function() { + const track = createMemoryTrack.call(this, undefined); + + assert.isFalse(track.hasVisibleContent); + }); + + test('hasVisibleContent_falseWithNoThreadSlice', function() { + const sliceA = new tr.model.ThreadSlice('', title, 0, 7.6211, {}); + const model = buildModel([[sliceA]]); + const track = createMemoryTrack.call(this, model); + + assert.isFalse(track.hasVisibleContent); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html new file mode 100644 index 00000000000..45404899b7c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html @@ -0,0 +1,534 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/ui/base/draw_helpers.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/alert_track.html"> +<link rel="import" href="/tracing/ui/tracks/container_track.html"> +<link rel="import" href="/tracing/ui/tracks/cpu_usage_track.html"> +<link rel="import" href="/tracing/ui/tracks/device_track.html"> +<link rel="import" href="/tracing/ui/tracks/global_memory_dump_track.html"> +<link rel="import" href="/tracing/ui/tracks/interaction_track.html"> +<link rel="import" href="/tracing/ui/tracks/kernel_track.html"> +<link rel="import" href="/tracing/ui/tracks/memory_track.html"> +<link rel="import" href="/tracing/ui/tracks/process_track.html"> + +<style> +.model-track { + flex-grow: 1; +} +</style> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const SelectionState = tr.model.SelectionState; + const ColorScheme = tr.b.ColorScheme; + const EventPresenter = tr.ui.b.EventPresenter; + + /** + * Visualizes a Model by building ProcessTracks and CpuTracks. + * @constructor + */ + const ModelTrack = tr.ui.b.define('model-track', tr.ui.tracks.ContainerTrack); + + ModelTrack.VSYNC_HIGHLIGHT_ALPHA = 0.1; + ModelTrack.VSYNC_DENSITY_TRANSPARENT = 0.20; + ModelTrack.VSYNC_DENSITY_OPAQUE = 0.10; + ModelTrack.VSYNC_DENSITY_RANGE = + ModelTrack.VSYNC_DENSITY_TRANSPARENT - ModelTrack.VSYNC_DENSITY_OPAQUE; + + /** + * Generate a zebra striping from a list of times. + * + * @param {!Array.<number>} times A sorted array of timestamps. + * @param {number} minTime the lower bound of time to start striping at. + * @param {number} maxTime the upper bound of time to stop striping at. + * of |times|. + * + * @returns {!Array.<tr.b.math.Range>} An array of ranges where each element + * represents the time range that a stripe covers. Each range is a subset + * of the interval [minTime, maxTime]. + */ + ModelTrack.generateStripes_ = function(times, minTime, maxTime) { + if (times.length === 0) return []; + + // Find the lowest and highest index within the viewport. + const lowIndex = tr.b.findLowIndexInSortedArray( + times, (x => x), minTime); + let highIndex = lowIndex - 1; + while (times[highIndex + 1] <= maxTime) { + highIndex++; + } + + const stripes = []; + // Must start at an even index and end at an odd index. + for (let i = lowIndex - (lowIndex % 2); i <= highIndex; i += 2) { + const left = i < lowIndex ? minTime : times[i]; + const right = i + 1 > highIndex ? maxTime : times[i + 1]; + stripes.push(tr.b.math.Range.fromExplicitRange(left, right)); + } + + return stripes; + }; + + + ModelTrack.prototype = { + + __proto__: tr.ui.tracks.ContainerTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('model-track'); + + this.upperMode_ = false; + this.annotationViews_ = []; + this.vSyncTimes_ = []; + }, + + get processViews() { + return Polymer.dom(this).querySelectorAll('.process-track-base'); + }, + + // upperMode is true if the track is being used on the ruler. + get upperMode() { + return this.upperMode_; + }, + + set upperMode(upperMode) { + this.upperMode_ = upperMode; + this.updateContents_(); + }, + + detach() { + tr.ui.tracks.ContainerTrack.prototype.detach.call(this); + }, + + get model() { + return this.model_; + }, + + set model(model) { + this.model_ = model; + this.updateContents_(); + + this.model_.addEventListener('annotationChange', + this.updateAnnotations_.bind(this)); + }, + + get hasVisibleContent() { + return this.children.length > 0; + }, + + updateContents_() { + Polymer.dom(this).textContent = ''; + if (!this.model_) return; + + if (this.upperMode_) { + this.updateContentsForUpperMode_(); + } else { + this.updateContentsForLowerMode_(); + } + }, + + updateContentsForUpperMode_() { + }, + + updateContentsForLowerMode_() { + if (this.model_.userModel.expectations.length > 1) { + const mrt = new tr.ui.tracks.InteractionTrack(this.viewport_); + mrt.model = this.model_; + Polymer.dom(this).appendChild(mrt); + } + + if (this.model_.alerts.length) { + const at = new tr.ui.tracks.AlertTrack(this.viewport_); + at.alerts = this.model_.alerts; + Polymer.dom(this).appendChild(at); + } + + if (this.model_.globalMemoryDumps.length) { + const gmdt = new tr.ui.tracks.GlobalMemoryDumpTrack(this.viewport_); + gmdt.memoryDumps = this.model_.globalMemoryDumps; + Polymer.dom(this).appendChild(gmdt); + } + + this.appendDeviceTrack_(); + this.appendCpuUsageTrack_(); + this.appendMemoryTrack_(); + this.appendKernelTrack_(); + + // Get a sorted list of processes. + const processes = this.model_.getAllProcesses(); + processes.sort(tr.model.Process.compare); + + for (let i = 0; i < processes.length; ++i) { + const process = processes[i]; + + const track = new tr.ui.tracks.ProcessTrack(this.viewport); + track.process = process; + if (!track.hasVisibleContent) continue; + + Polymer.dom(this).appendChild(track); + } + this.viewport_.rebuildEventToTrackMap(); + this.viewport_.rebuildContainerToTrackMap(); + this.vSyncTimes_ = this.model_.device.vSyncTimestamps; + + this.updateAnnotations_(); + }, + + getContentBounds() { return this.model.bounds; }, + + addAnnotation(annotation) { + this.model.addAnnotation(annotation); + }, + + removeAnnotation(annotation) { + this.model.removeAnnotation(annotation); + }, + + updateAnnotations_() { + this.annotationViews_ = []; + const annotations = this.model_.getAllAnnotations(); + for (let i = 0; i < annotations.length; i++) { + this.annotationViews_.push( + annotations[i].getOrCreateView(this.viewport_)); + } + this.invalidateDrawingContainer(); + }, + + addEventsToTrackMap(eventToTrackMap) { + if (!this.model_) return; + + const tracks = this.children; + for (let i = 0; i < tracks.length; ++i) { + tracks[i].addEventsToTrackMap(eventToTrackMap); + } + + if (this.instantEvents === undefined) return; + + const vp = this.viewport_; + this.instantEvents.forEach(function(ev) { + eventToTrackMap.addEvent(ev, this); + }.bind(this)); + }, + + appendDeviceTrack_() { + const device = this.model.device; + const track = new tr.ui.tracks.DeviceTrack(this.viewport); + track.device = this.model.device; + if (!track.hasVisibleContent) return; + Polymer.dom(this).appendChild(track); + }, + + appendKernelTrack_() { + const kernel = this.model.kernel; + const track = new tr.ui.tracks.KernelTrack(this.viewport); + track.kernel = this.model.kernel; + if (!track.hasVisibleContent) return; + Polymer.dom(this).appendChild(track); + }, + + appendCpuUsageTrack_() { + const track = new tr.ui.tracks.CpuUsageTrack(this.viewport); + track.initialize(this.model); + if (!track.hasVisibleContent) return; + + this.appendChild(track); + }, + + appendMemoryTrack_() { + const track = new tr.ui.tracks.MemoryTrack(this.viewport); + track.initialize(this.model); + if (!track.hasVisibleContent) return; + + Polymer.dom(this).appendChild(track); + }, + + drawTrack(type) { + const ctx = this.context(); + if (!this.model_) return; + + const pixelRatio = window.devicePixelRatio || 1; + const bounds = this.getBoundingClientRect(); + const canvasBounds = ctx.canvas.getBoundingClientRect(); + + ctx.save(); + ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top)); + + const dt = this.viewport.currentDisplayTransform; + const viewLWorld = dt.xViewToWorld(0); + const viewRWorld = dt.xViewToWorld(canvasBounds.width * pixelRatio); + const viewHeight = bounds.height * pixelRatio; + + switch (type) { + case tr.ui.tracks.DrawType.GRID: + this.viewport.drawMajorMarkLines(ctx, viewHeight); + // The model is the only thing that draws grid lines. + ctx.restore(); + return; + + case tr.ui.tracks.DrawType.FLOW_ARROWS: + if (this.model_.flowIntervalTree.size === 0) { + ctx.restore(); + return; + } + + this.drawFlowArrows_(viewLWorld, viewRWorld); + ctx.restore(); + return; + + case tr.ui.tracks.DrawType.INSTANT_EVENT: + if (!this.model_.instantEvents || + this.model_.instantEvents.length === 0) { + break; + } + + tr.ui.b.drawInstantSlicesAsLines( + ctx, + this.viewport.currentDisplayTransform, + viewLWorld, + viewRWorld, + bounds.height, + this.model_.instantEvents, + 4); + + break; + + case tr.ui.tracks.DrawType.MARKERS: + if (!this.viewport.interestRange.isEmpty) { + this.viewport.interestRange.draw( + ctx, viewLWorld, viewRWorld, viewHeight); + this.viewport.interestRange.drawIndicators( + ctx, viewLWorld, viewRWorld); + } + ctx.restore(); + return; + + case tr.ui.tracks.DrawType.HIGHLIGHTS: + this.drawVSyncHighlight( + ctx, dt, viewLWorld, viewRWorld, viewHeight); + ctx.restore(); + return; + + case tr.ui.tracks.DrawType.ANNOTATIONS: + for (let i = 0; i < this.annotationViews_.length; i++) { + this.annotationViews_[i].draw(ctx); + } + ctx.restore(); + return; + } + ctx.restore(); + + tr.ui.tracks.ContainerTrack.prototype.drawTrack.call(this, type); + }, + + drawFlowArrows_(viewLWorld, viewRWorld) { + const ctx = this.context(); + + ctx.strokeStyle = 'rgba(0, 0, 0, 0.4)'; + ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'; + ctx.lineWidth = 1; + + const events = + this.model_.flowIntervalTree.findIntersection(viewLWorld, viewRWorld); + + // When not showing flow events, show only highlighted/selected ones. + const onlyHighlighted = !this.viewport.showFlowEvents; + const canvasBounds = ctx.canvas.getBoundingClientRect(); + for (let i = 0; i < events.length; ++i) { + if (onlyHighlighted && + events[i].selectionState !== SelectionState.SELECTED && + events[i].selectionState !== SelectionState.HIGHLIGHTED) { + continue; + } + this.drawFlowArrow_(ctx, events[i], canvasBounds); + } + }, + + drawFlowArrow_(ctx, flowEvent, canvasBounds) { + const dt = this.viewport.currentDisplayTransform; + const pixelRatio = window.devicePixelRatio || 1; + + const startTrack = this.viewport.trackForEvent(flowEvent.startSlice); + const endTrack = this.viewport.trackForEvent(flowEvent.endSlice); + + // TODO(nduca): Figure out how to draw flow arrows even when + // processes are collapsed, bug #931. + if (startTrack === undefined || endTrack === undefined) return; + + const startBounds = startTrack.getBoundingClientRect(); + const endBounds = endTrack.getBoundingClientRect(); + + if (flowEvent.selectionState === SelectionState.SELECTED) { + ctx.shadowBlur = 1; + ctx.shadowColor = 'red'; + ctx.shadowOffsety = 2; + ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[ + tr.b.ColorScheme.getVariantColorId( + flowEvent.colorId, + tr.b.ColorScheme.properties.brightenedOffsets[0])]; + } else if (flowEvent.selectionState === SelectionState.HIGHLIGHTED) { + ctx.shadowBlur = 1; + ctx.shadowColor = 'red'; + ctx.shadowOffsety = 2; + ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[ + tr.b.ColorScheme.getVariantColorId( + flowEvent.colorId, + tr.b.ColorScheme.properties.brightenedOffsets[0])]; + } else if (flowEvent.selectionState === SelectionState.DIMMED) { + ctx.shadowBlur = 0; + ctx.shadowOffsetX = 0; + ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[flowEvent.colorId]; + } else { + let hasBoost = false; + const startSlice = flowEvent.startSlice; + hasBoost |= startSlice.selectionState === SelectionState.SELECTED; + hasBoost |= startSlice.selectionState === SelectionState.HIGHLIGHTED; + const endSlice = flowEvent.endSlice; + hasBoost |= endSlice.selectionState === SelectionState.SELECTED; + hasBoost |= endSlice.selectionState === SelectionState.HIGHLIGHTED; + if (hasBoost) { + ctx.shadowBlur = 1; + ctx.shadowColor = 'rgba(255, 0, 0, 0.4)'; + ctx.shadowOffsety = 2; + ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[ + tr.b.ColorScheme.getVariantColorId( + flowEvent.colorId, + tr.b.ColorScheme.properties.brightenedOffsets[0])]; + } else { + ctx.shadowBlur = 0; + ctx.shadowOffsetX = 0; + ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[flowEvent.colorId]; + } + } + + const startSize = startBounds.left + startBounds.top + + startBounds.bottom + startBounds.right; + const endSize = endBounds.left + endBounds.top + + endBounds.bottom + endBounds.right; + // Nothing to do if both ends of the track are collapsed. + if (startSize === 0 && endSize === 0) return; + + const startY = this.calculateTrackY_(startTrack, canvasBounds); + const endY = this.calculateTrackY_(endTrack, canvasBounds); + + const pixelStartY = pixelRatio * startY; + const pixelEndY = pixelRatio * endY; + + const startXView = dt.xWorldToView(flowEvent.start); + const endXView = dt.xWorldToView(flowEvent.end); + const midXView = (startXView + endXView) / 2; + + ctx.beginPath(); + ctx.moveTo(startXView, pixelStartY); + ctx.bezierCurveTo( + midXView, pixelStartY, + midXView, pixelEndY, + endXView, pixelEndY); + ctx.stroke(); + + const arrowWidth = 5 * pixelRatio; + const distance = endXView - startXView; + if (distance <= (2 * arrowWidth)) return; + + const tipX = endXView; + const tipY = pixelEndY; + const arrowHeight = (endBounds.height / 4) * pixelRatio; + tr.ui.b.drawTriangle(ctx, + tipX, tipY, + tipX - arrowWidth, tipY - arrowHeight, + tipX - arrowWidth, tipY + arrowHeight); + ctx.fill(); + }, + + /** + * Highlights VSync events on the model track (using "zebra" striping). + */ + drawVSyncHighlight(ctx, dt, viewLWorld, viewRWorld, viewHeight) { + if (!this.viewport_.highlightVSync) { + return; + } + + const stripes = ModelTrack.generateStripes_( + this.vSyncTimes_, viewLWorld, viewRWorld); + if (stripes.length === 0) { + return; + } + + const vSyncHighlightColor = + new tr.b.Color(ColorScheme.getColorForReservedNameAsString( + 'vsync_highlight_color')); + + const stripeRange = stripes[stripes.length - 1].max - stripes[0].min; + const stripeDensity = + stripeRange ? stripes.length / (dt.scaleX * stripeRange) : 0; + const clampedStripeDensity = + tr.b.math.clamp(stripeDensity, ModelTrack.VSYNC_DENSITY_OPAQUE, + ModelTrack.VSYNC_DENSITY_TRANSPARENT); + const opacity = + (ModelTrack.VSYNC_DENSITY_TRANSPARENT - clampedStripeDensity) / + ModelTrack.VSYNC_DENSITY_RANGE; + if (opacity === 0) { + return; + } + + ctx.fillStyle = vSyncHighlightColor.toStringWithAlphaOverride( + ModelTrack.VSYNC_HIGHLIGHT_ALPHA * opacity); + + for (let i = 0; i < stripes.length; i++) { + const xLeftView = dt.xWorldToView(stripes[i].min); + const xRightView = dt.xWorldToView(stripes[i].max); + ctx.fillRect(xLeftView, 0, xRightView - xLeftView, viewHeight); + } + }, + + calculateTrackY_(track, canvasBounds) { + const bounds = track.getBoundingClientRect(); + const size = bounds.left + bounds.top + bounds.bottom + bounds.right; + if (size === 0) { + return this.calculateTrackY_( + Polymer.dom(track).parentNode, canvasBounds); + } + + return bounds.top - canvasBounds.top + (bounds.height / 2); + }, + + addIntersectingEventsInRangeToSelectionInWorldSpace( + loWX, hiWX, viewPixWidthWorld, selection) { + function onPickHit(instantEvent) { + selection.push(instantEvent); + } + const instantEventWidth = 3 * viewPixWidthWorld; + tr.b.iterateOverIntersectingIntervals(this.model_.instantEvents, + function(x) { return x.start; }, + function(x) { return x.duration + instantEventWidth; }, + loWX, hiWX, + onPickHit.bind(this)); + + tr.ui.tracks.ContainerTrack.prototype. + addIntersectingEventsInRangeToSelectionInWorldSpace. + apply(this, arguments); + }, + + addClosestEventToSelection(worldX, worldMaxDist, loY, hiY, + selection) { + this.addClosestInstantEventToSelection(this.model_.instantEvents, + worldX, worldMaxDist, selection); + tr.ui.tracks.ContainerTrack.prototype.addClosestEventToSelection. + apply(this, arguments); + } + }; + + return { + ModelTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html new file mode 100644 index 00000000000..53e19146ae2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html @@ -0,0 +1,178 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/thread.html"> +<link rel="import" href="/tracing/ui/tracks/model_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Range = tr.b.math.Range; + const VIEW_L_WORLD = 100; + const VIEW_R_WORLD = 1000; + + function testGenerateStripes(times, expectedRanges) { + const ranges = tr.ui.tracks.ModelTrack.generateStripes_( + times, VIEW_L_WORLD, VIEW_R_WORLD); + + assert.sameDeepMembers(ranges, expectedRanges); + } + + test('generateStripesInside', function() { + const range200To500 = Range.fromExplicitRange(200, 500); + const range800To900 = Range.fromExplicitRange(800, 900); + const range998To999 = Range.fromExplicitRange(998, 999); + testGenerateStripes([], []); + testGenerateStripes([200, 500], [range200To500]); + testGenerateStripes([200, 500, 800, 900], [range200To500, range800To900]); + testGenerateStripes( + [200, 500, 800, 900, 998, 999], + [range200To500, range800To900, range998To999]); + }); + + test('generateStripesOutside', function() { + const range101To999 = Range.fromExplicitRange(101, 999); + // Far left. + testGenerateStripes([0, 99], []); + testGenerateStripes([0, 10, 50, 99], []); + testGenerateStripes([0, 99, 101, 999], [range101To999]); + testGenerateStripes([0, 10, 50, 99, 101, 999], [range101To999]); + + // Far right. + testGenerateStripes([1001, 2000], []); + testGenerateStripes([1001, 2000, 3000, 4000], []); + testGenerateStripes([101, 999, 1001, 2000], [range101To999]); + testGenerateStripes([101, 999, 1001, 2000, 3000, 4000], [range101To999]); + + // Far both. + testGenerateStripes([0, 99, 1001, 2000], []); + testGenerateStripes([0, 10, 50, 99, 1001, 2000], []); + testGenerateStripes([0, 10, 50, 99, 1001, 2000, 3000, 4000], []); + testGenerateStripes([0, 99, 101, 999, 1001, 2000], [range101To999]); + }); + + test('generateStripesOverlap', function() { + const rangeLeftWorldTo101 = Range.fromExplicitRange(VIEW_L_WORLD, 101); + const range102To103 = Range.fromExplicitRange(102, 103); + const range200To900 = Range.fromExplicitRange(200, 900); + const range997To998 = Range.fromExplicitRange(997, 998); + const range999ToRightWorld = Range.fromExplicitRange(999, VIEW_R_WORLD); + const rangeLeftWorldToRightWorld = + Range.fromExplicitRange(VIEW_L_WORLD, VIEW_R_WORLD); + + + // Left overlap. + testGenerateStripes([0, 101], [rangeLeftWorldTo101]); + testGenerateStripes([0, 1, 2, 101], [rangeLeftWorldTo101]); + testGenerateStripes( + [2, 101, 102, 103], + [rangeLeftWorldTo101, range102To103]); + testGenerateStripes( + [0, 1, 2, 101, 102, 103], + [rangeLeftWorldTo101, range102To103]); + testGenerateStripes( + [0, 1, 2, 101, 102, 103, 1001, 3000], + [rangeLeftWorldTo101, range102To103]); + + // Right overlap. + testGenerateStripes([999, 2000], [range999ToRightWorld]); + testGenerateStripes([999, 2000, 3000, 4000], [range999ToRightWorld]); + testGenerateStripes( + [997, 998, 999, 2000], + [range997To998, range999ToRightWorld]); + testGenerateStripes( + [997, 998, 999, 2000, 3000, 4000], + [range997To998, range999ToRightWorld]); + testGenerateStripes( + [0, 10, 997, 998, 999, 2000, 3000, 4000], + [range997To998, range999ToRightWorld]); + + // Both overlap. + testGenerateStripes([0, 2000], [rangeLeftWorldToRightWorld]); + testGenerateStripes( + [0, 101, 999, 2000], + [rangeLeftWorldTo101, range999ToRightWorld]); + testGenerateStripes( + [0, 101, 200, 900, 999, 2000], + [rangeLeftWorldTo101, range200To900, range999ToRightWorld]); + testGenerateStripes( + [0, 10, 90, 101, 999, 2000, 3000, 4000], + [rangeLeftWorldTo101, range999ToRightWorld]); + testGenerateStripes( + [0, 10, 90, 101, 200, 900, 999, 2000, 3000, 4000], + [rangeLeftWorldTo101, range200To900, range999ToRightWorld]); + }); + + test('generateStripesOdd', function() { + const range500To900 = Range.fromExplicitRange(500, 900); + const rangeLeftWorldTo200 = Range.fromExplicitRange(VIEW_L_WORLD, 200); + const rangeLeftWorldTo500 = Range.fromExplicitRange(VIEW_L_WORLD, 500); + const range500ToRightWorld = Range.fromExplicitRange(500, VIEW_R_WORLD); + const rangeLeftWorldToRightWorld = + Range.fromExplicitRange(VIEW_L_WORLD, VIEW_R_WORLD); + + // One VSync. + testGenerateStripes([0], [rangeLeftWorldToRightWorld]); + testGenerateStripes([500], [range500ToRightWorld]); + testGenerateStripes([1500], []); + + // Multiple VSyncs. + testGenerateStripes([0, 10, 20], [rangeLeftWorldToRightWorld]); + testGenerateStripes([0, 500, 2000], [rangeLeftWorldTo500]); + testGenerateStripes([0, 10, 500], [range500ToRightWorld]); + testGenerateStripes([0, 10, 2000], []); + testGenerateStripes( + [0, 200, 500], + [rangeLeftWorldTo200, range500ToRightWorld]); + testGenerateStripes( + [0, 200, 500, 900], + [rangeLeftWorldTo200, range500To900]); + }); + + test('generateStripesBorder', function() { + const rangeLeftWorldToLeftWorld = + Range.fromExplicitRange(VIEW_L_WORLD, VIEW_L_WORLD); + const rangeRightWorldToRightWorld = + Range.fromExplicitRange(VIEW_R_WORLD, VIEW_R_WORLD); + const rangeLeftWorldToRightWorld = + Range.fromExplicitRange(VIEW_L_WORLD, VIEW_R_WORLD); + const rangeLeftWorldTo200 = Range.fromExplicitRange(VIEW_L_WORLD, 200); + const range200To500 = Range.fromExplicitRange(200, 500); + const range500ToRightWorld = Range.fromExplicitRange(500, VIEW_R_WORLD); + testGenerateStripes([0, VIEW_L_WORLD], [rangeLeftWorldToLeftWorld]); + testGenerateStripes( + [VIEW_L_WORLD, VIEW_L_WORLD], + [rangeLeftWorldToLeftWorld]); + testGenerateStripes( + [VIEW_R_WORLD, 2000], + [rangeRightWorldToRightWorld]); + testGenerateStripes( + [VIEW_R_WORLD, VIEW_R_WORLD], + [rangeRightWorldToRightWorld]); + testGenerateStripes( + [VIEW_L_WORLD, VIEW_R_WORLD], + [rangeLeftWorldToRightWorld]); + testGenerateStripes( + [VIEW_L_WORLD, 200, 500, VIEW_R_WORLD], + [rangeLeftWorldTo200, range500ToRightWorld]); + testGenerateStripes( + [0, VIEW_L_WORLD, VIEW_R_WORLD, 2000], + [rangeLeftWorldToLeftWorld, rangeRightWorldToRightWorld]); + testGenerateStripes( + [0, VIEW_L_WORLD, VIEW_R_WORLD, 2000], + [rangeLeftWorldToLeftWorld, rangeRightWorldToRightWorld]); + testGenerateStripes( + [0, VIEW_L_WORLD, 200, 500, VIEW_R_WORLD, 2000], + [rangeLeftWorldToLeftWorld, range200To500, + rangeRightWorldToRightWorld]); + testGenerateStripes( + [0, 10, VIEW_L_WORLD, VIEW_R_WORLD, 2000, 3000], + [rangeLeftWorldToRightWorld]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html new file mode 100644 index 00000000000..8b5fd837f0d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html @@ -0,0 +1,240 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/model_settings.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/container_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays a group of objects in multiple rows. + * @constructor + * @extends {ContainerTrack} + */ + const MultiRowTrack = tr.ui.b.define( + 'multi-row-track', tr.ui.tracks.ContainerTrack); + + MultiRowTrack.prototype = { + + __proto__: tr.ui.tracks.ContainerTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport); + this.tooltip_ = ''; + this.heading_ = ''; + + this.groupingSource_ = undefined; + this.itemsToGroup_ = undefined; + + this.defaultToCollapsedWhenSubRowCountMoreThan = 1; + + this.currentSubRowsWithHeadings_ = undefined; + this.expanded_ = true; + }, + + get itemsToGroup() { + return this.itemsToGroup_; + }, + + setItemsToGroup(itemsToGroup, opt_groupingSource) { + this.itemsToGroup_ = itemsToGroup; + this.groupingSource_ = opt_groupingSource; + this.currentSubRowsWithHeadings_ = undefined; + this.updateContents_(); + this.updateExpandedStateFromGroupingSource_(); + }, + + /** + * Opt-out from using buildSubRows_() and provide prebuilt rows. + * Array of {row: [rowItems...], heading} dicts is expected as an argument. + */ + setPrebuiltSubRows(groupingSource, subRowsWithHeadings) { + this.itemsToGroup_ = undefined; + this.groupingSource_ = groupingSource; + this.currentSubRowsWithHeadings_ = subRowsWithHeadings; + this.updateContents_(); + this.updateExpandedStateFromGroupingSource_(); + }, + + get heading() { + return this.heading_; + }, + + set heading(h) { + this.heading_ = h; + this.updateHeadingAndTooltip_(); + }, + + get tooltip() { + return this.tooltip_; + }, + + set tooltip(t) { + this.tooltip_ = t; + this.updateHeadingAndTooltip_(); + }, + + get subRows() { + return this.currentSubRowsWithHeadings_.map(elem => elem.row); + }, + + get hasVisibleContent() { + return this.children.length > 0; + }, + + get expanded() { + return this.expanded_; + }, + + set expanded(expanded) { + if (this.expanded_ === expanded) return; + + this.expanded_ = expanded; + this.expandedStateChanged_(); + }, + + onHeadingClicked_(e) { + if (this.subRows.length <= 1) return; + + this.expanded = !this.expanded; + + if (this.groupingSource_) { + const modelSettings = new tr.model.ModelSettings( + this.groupingSource_.model); + modelSettings.setSettingFor(this.groupingSource_, 'expanded', + this.expanded); + } + + e.stopPropagation(); + }, + + updateExpandedStateFromGroupingSource_() { + if (this.groupingSource_) { + const numSubRows = this.subRows.length; + const modelSettings = new tr.model.ModelSettings( + this.groupingSource_.model); + if (numSubRows > 1) { + let defaultExpanded; + if (numSubRows > this.defaultToCollapsedWhenSubRowCountMoreThan) { + defaultExpanded = false; + } else { + defaultExpanded = true; + } + this.expanded = modelSettings.getSettingFor( + this.groupingSource_, 'expanded', defaultExpanded); + } else { + this.expanded = undefined; + } + } + }, + + expandedStateChanged_() { + const minH = Math.max(2, Math.ceil(18 / this.children.length)); + const h = (this.expanded_ ? 18 : minH) + 'px'; + + for (let i = 0; i < this.children.length; i++) { + this.children[i].height = h; + if (i === 0) { + this.children[i].arrowVisible = true; + } + this.children[i].expanded = this.expanded; + } + + if (this.children.length === 1) { + this.children[0].expanded = true; + this.children[0].arrowVisible = false; + } + }, + + updateContents_() { + tr.ui.tracks.ContainerTrack.prototype.updateContents_.call(this); + this.detach(); // Clear sub-tracks. + + if (this.currentSubRowsWithHeadings_ === undefined) { + // No prebuilt rows, build it. + if (this.itemsToGroup_ === undefined) { + return; + } + const subRows = this.buildSubRows_(this.itemsToGroup_); + this.currentSubRowsWithHeadings_ = subRows.map(row => { + return {row, heading: undefined}; + }); + } + if (this.currentSubRowsWithHeadings_ === undefined || + this.currentSubRowsWithHeadings_.length === 0) { + return; + } + + const addSubTrackEx = (items, opt_heading) => { + const track = this.addSubTrack_(items); + if (opt_heading !== undefined) { + track.heading = opt_heading; + } + track.addEventListener( + 'heading-clicked', this.onHeadingClicked_.bind(this)); + }; + + if (this.currentSubRowsWithHeadings_[0].heading !== undefined && + this.currentSubRowsWithHeadings_[0].heading !== this.heading_) { + // Create an empty row to render the group's title there. + addSubTrackEx([]); + } + + for (const subRowWithHeading of this.currentSubRowsWithHeadings_) { + const subRow = subRowWithHeading.row; + if (subRow.length === 0) { + continue; + } + addSubTrackEx(subRow, subRowWithHeading.heading); + } + + this.updateHeadingAndTooltip_(); + this.expandedStateChanged_(); + }, + + updateHeadingAndTooltip_() { + if (!Polymer.dom(this).firstChild) return; + + Polymer.dom(this).firstChild.heading = this.heading_; + Polymer.dom(this).firstChild.tooltip = this.tooltip_; + }, + + /** + * Breaks up the list of slices into N rows, each of which is a list of + * slices that are non overlapping. + */ + buildSubRows_(itemsToGroup) { + throw new Error('Not implemented'); + }, + + addSubTrack_(subRowItems) { + throw new Error('Not implemented'); + }, + + areArrayContentsSame_(a, b) { + if (!a || !b) return false; + + if (!a.length || !b.length) return false; + + if (a.length !== b.length) return false; + + for (let i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) return false; + } + return true; + } + }; + + return { + MultiRowTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html new file mode 100644 index 00000000000..b97b48c3ef0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/analysis/object_instance_view.html"> +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/multi_row_track.html"> +<link rel="import" href="/tracing/ui/tracks/object_instance_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays a ObjectInstanceGroup. + * @constructor + * @extends {ContainerTrack} + */ + const ObjectInstanceGroupTrack = tr.ui.b.define( + 'object-instance-group-track', tr.ui.tracks.MultiRowTrack); + + ObjectInstanceGroupTrack.prototype = { + + __proto__: tr.ui.tracks.MultiRowTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('object-instance-group-track'); + this.objectInstances_ = undefined; + }, + + get objectInstances() { + return this.itemsToGroup; + }, + + set objectInstances(objectInstances) { + this.setItemsToGroup(objectInstances); + }, + + addSubTrack_(objectInstances) { + const hasMultipleRows = this.subRows.length > 1; + const track = new tr.ui.tracks.ObjectInstanceTrack(this.viewport); + track.objectInstances = objectInstances; + Polymer.dom(this).appendChild(track); + return track; + }, + + buildSubRows_(objectInstances) { + objectInstances.sort(function(x, y) { + return x.creationTs - y.creationTs; + }); + + const subRows = []; + for (let i = 0; i < objectInstances.length; i++) { + const objectInstance = objectInstances[i]; + + let found = false; + for (let j = 0; j < subRows.length; j++) { + const subRow = subRows[j]; + const lastItemInSubRow = subRow[subRow.length - 1]; + if (objectInstance.creationTs >= lastItemInSubRow.deletionTs) { + found = true; + subRow.push(objectInstance); + break; + } + } + if (!found) { + subRows.push([objectInstance]); + } + } + return subRows; + }, + updateHeadingAndTooltip_() { + } + }; + + return { + ObjectInstanceGroupTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css new file mode 100644 index 00000000000..0919e85524e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css @@ -0,0 +1,8 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +.object-instance-track { + height: 18px; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html new file mode 100644 index 00000000000..f21a87d04db --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html @@ -0,0 +1,294 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="stylesheet" href="/tracing/ui/tracks/object_instance_track.css"> + +<link rel="import" href="/tracing/base/extension_registry.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event.html"> +<link rel="import" href="/tracing/ui/base/event_presenter.html"> +<link rel="import" href="/tracing/ui/base/heading.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const SelectionState = tr.model.SelectionState; + const EventPresenter = tr.ui.b.EventPresenter; + + /** + * A track that displays an array of Slice objects. + * @constructor + * @extends {Track} + */ + const ObjectInstanceTrack = tr.ui.b.define( + 'object-instance-track', tr.ui.tracks.Track); + + ObjectInstanceTrack.prototype = { + __proto__: tr.ui.tracks.Track.prototype, + + decorate(viewport) { + tr.ui.tracks.Track.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('object-instance-track'); + this.objectInstances_ = []; + this.objectSnapshots_ = []; + + this.heading_ = document.createElement('tr-ui-b-heading'); + Polymer.dom(this).appendChild(this.heading_); + }, + + set heading(heading) { + this.heading_.heading = heading; + }, + + get heading() { + return this.heading_.heading; + }, + + set tooltip(tooltip) { + this.heading_.tooltip = tooltip; + }, + + get objectInstances() { + return this.objectInstances_; + }, + + set objectInstances(objectInstances) { + if (!objectInstances || objectInstances.length === 0) { + this.heading = ''; + this.objectInstances_ = []; + this.objectSnapshots_ = []; + return; + } + this.heading = objectInstances[0].baseTypeName; + this.objectInstances_ = objectInstances; + this.objectSnapshots_ = []; + this.objectInstances_.forEach(function(instance) { + this.objectSnapshots_.push.apply( + this.objectSnapshots_, instance.snapshots); + }, this); + this.objectSnapshots_.sort(function(a, b) { + return a.ts - b.ts; + }); + }, + + get height() { + return window.getComputedStyle(this).height; + }, + + set height(height) { + this.style.height = height; + }, + + get snapshotRadiusView() { + return 7 * (window.devicePixelRatio || 1); + }, + + draw(type, viewLWorld, viewRWorld, viewHeight) { + switch (type) { + case tr.ui.tracks.DrawType.GENERAL_EVENT: + this.drawObjectInstances_(viewLWorld, viewRWorld); + break; + } + }, + + drawObjectInstances_(viewLWorld, viewRWorld) { + const ctx = this.context(); + const pixelRatio = window.devicePixelRatio || 1; + + const bounds = this.getBoundingClientRect(); + const height = bounds.height * pixelRatio; + const halfHeight = height * 0.5; + const twoPi = Math.PI * 2; + + // Culling parameters. + const dt = this.viewport.currentDisplayTransform; + const snapshotRadiusView = this.snapshotRadiusView; + const snapshotRadiusWorld = dt.xViewVectorToWorld(height); + + // Instances + const objectInstances = this.objectInstances_; + let loI = tr.b.findLowIndexInSortedArray( + objectInstances, + function(instance) { + return instance.deletionTs; + }, + viewLWorld); + ctx.save(); + ctx.strokeStyle = 'rgb(0,0,0)'; + for (let i = loI; i < objectInstances.length; ++i) { + const instance = objectInstances[i]; + const x = instance.creationTs; + if (x > viewRWorld) break; + + const right = instance.deletionTs === Number.MAX_VALUE ? + viewRWorld : instance.deletionTs; + const xView = dt.xWorldToView(x); + const widthView = dt.xWorldVectorToView(right - x); + ctx.fillStyle = EventPresenter.getObjectInstanceColor(instance); + ctx.fillRect(xView, pixelRatio, widthView, height - 2 * pixelRatio); + } + ctx.restore(); + + // Snapshots. Has to run in worldspace because ctx.arc gets transformed. + const objectSnapshots = this.objectSnapshots_; + loI = tr.b.findLowIndexInSortedArray( + objectSnapshots, + function(snapshot) { + return snapshot.ts + snapshotRadiusWorld; + }, + viewLWorld); + for (let i = loI; i < objectSnapshots.length; ++i) { + const snapshot = objectSnapshots[i]; + const x = snapshot.ts; + if (x - snapshotRadiusWorld > viewRWorld) break; + + const xView = dt.xWorldToView(x); + + ctx.fillStyle = EventPresenter.getObjectSnapshotColor(snapshot); + ctx.beginPath(); + ctx.arc(xView, halfHeight, snapshotRadiusView, 0, twoPi); + ctx.fill(); + if (snapshot.selected) { + ctx.lineWidth = 5; + ctx.strokeStyle = 'rgb(100,100,0)'; + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(xView, halfHeight, snapshotRadiusView - 1, 0, twoPi); + ctx.lineWidth = 2; + ctx.strokeStyle = 'rgb(255,255,0)'; + ctx.stroke(); + } else { + ctx.lineWidth = 1; + ctx.strokeStyle = 'rgb(0,0,0)'; + ctx.stroke(); + } + } + ctx.lineWidth = 1; + + // For performance reasons we only check the SelectionState of the first + // instance. If it's DIMMED we assume that all are DIMMED. + // TODO(egraether): Allow partial highlight. + let selectionState = SelectionState.NONE; + if (objectInstances.length && + objectInstances[0].selectionState === SelectionState.DIMMED) { + selectionState = SelectionState.DIMMED; + } + + // Dim the track when there is an active highlight. + if (selectionState === SelectionState.DIMMED) { + const width = bounds.width * pixelRatio; + ctx.fillStyle = 'rgba(255,255,255,0.5)'; + ctx.fillRect(0, 0, width, height); + ctx.restore(); + } + }, + + addEventsToTrackMap(eventToTrackMap) { + if (this.objectInstance_ !== undefined) { + this.objectInstance_.forEach(function(obj) { + eventToTrackMap.addEvent(obj, this); + }, this); + } + + if (this.objectSnapshots_ !== undefined) { + this.objectSnapshots_.forEach(function(obj) { + eventToTrackMap.addEvent(obj, this); + }, this); + } + }, + + addIntersectingEventsInRangeToSelectionInWorldSpace( + loWX, hiWX, viewPixWidthWorld, selection) { + // Pick snapshots first. + let foundSnapshot = false; + function onSnapshot(snapshot) { + selection.push(snapshot); + foundSnapshot = true; + } + const snapshotRadiusView = this.snapshotRadiusView; + const snapshotRadiusWorld = viewPixWidthWorld * snapshotRadiusView; + tr.b.iterateOverIntersectingIntervals( + this.objectSnapshots_, + function(x) { return x.ts - snapshotRadiusWorld; }, + function(x) { return 2 * snapshotRadiusWorld; }, + loWX, hiWX, + onSnapshot); + if (foundSnapshot) return; + + // Try picking instances. + tr.b.iterateOverIntersectingIntervals( + this.objectInstances_, + function(x) { return x.creationTs; }, + function(x) { return x.deletionTs - x.creationTs; }, + loWX, hiWX, + (value) => { selection.push(value); }); + }, + + /** + * Add the item to the left or right of the provided event, if any, to the + * selection. + * @param {event} The current event item. + * @param {Number} offset Number of slices away from the event to look. + * @param {Selection} selection The selection to add an event to, + * if found. + * @return {boolean} Whether an event was found. + * @private + */ + addEventNearToProvidedEventToSelection(event, offset, selection) { + let events; + if (event instanceof tr.model.ObjectSnapshot) { + events = this.objectSnapshots_; + } else if (event instanceof tr.model.ObjectInstance) { + events = this.objectInstances_; + } else { + throw new Error('Unrecognized event'); + } + + const index = events.indexOf(event); + const newIndex = index + offset; + if (newIndex >= 0 && newIndex < events.length) { + selection.push(events[newIndex]); + return true; + } + return false; + }, + + addAllEventsMatchingFilterToSelection(filter, selection) { + }, + + addClosestEventToSelection(worldX, worldMaxDist, loY, hiY, + selection) { + const snapshot = tr.b.findClosestElementInSortedArray( + this.objectSnapshots_, + function(x) { return x.ts; }, + worldX, + worldMaxDist); + + if (!snapshot) return; + + selection.push(snapshot); + + // TODO(egraether): Search for object instances as well, which was not + // implemented because it makes little sense with the current visual and + // needs to take care of overlapping intervals. + } + }; + + + const options = new tr.b.ExtensionRegistryOptions( + tr.b.TYPE_BASED_REGISTRY_MODE); + tr.b.decorateExtensionRegistry(ObjectInstanceTrack, options); + + return { + ObjectInstanceTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html new file mode 100644 index 00000000000..8312d0ba7e5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html @@ -0,0 +1,111 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/object_collection.html"> +<link rel="import" href="/tracing/model/scoped_id.html"> +<link rel="import" href="/tracing/model/selection_state.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> +<link rel="import" href="/tracing/ui/tracks/object_instance_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const EventSet = tr.model.EventSet; + const ObjectInstanceTrack = tr.ui.tracks.ObjectInstanceTrack; + const Viewport = tr.ui.TimelineViewport; + + const createObjects = function() { + const objects = new tr.model.ObjectCollection({}); + const scopedId1 = new tr.model.ScopedId('ptr', '0x1000'); + objects.idWasCreated(scopedId1, 'tr.e.cc', 'Frame', 10); + objects.addSnapshot(scopedId1, 'tr.e.cc', 'Frame', 10, 'snapshot-1'); + objects.addSnapshot(scopedId1, 'tr.e.cc', 'Frame', 25, 'snapshot-2'); + objects.addSnapshot(scopedId1, 'tr.e.cc', 'Frame', 40, 'snapshot-3'); + objects.idWasDeleted(scopedId1, 'tr.e.cc', 'Frame', 45); + + const scopedId2 = new tr.model.ScopedId('ptr', '0x1001'); + objects.idWasCreated(scopedId2, 'skia', 'Picture', 20); + objects.addSnapshot(scopedId2, 'skia', 'Picture', 20, 'snapshot-1'); + objects.idWasDeleted(scopedId2, 'skia', 'Picture', 25); + return objects; + }; + + test('instantiate', function() { + const objects = createObjects(); + const frames = objects.getAllInstancesByTypeName().Frame; + frames[0].snapshots[1].selectionState = + tr.model.SelectionState.SELECTED; + + const div = document.createElement('div'); + + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = ObjectInstanceTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + track.heading = 'testBasic'; + track.objectInstances = frames; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 50, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + }); + + test('selectionHitTestingWithThreadTrack', function() { + const objects = createObjects(); + const frames = objects.getAllInstancesByTypeName().Frame; + + const track = ObjectInstanceTrack(new Viewport()); + track.objectInstances = frames; + + // Hit outside range + let selection = new EventSet(); + track.addIntersectingEventsInRangeToSelectionInWorldSpace( + 8, 8.1, 0.1, selection); + assert.strictEqual(selection.length, 0); + + // Hit the first snapshot, via pixel-nearness. + selection = new EventSet(); + track.addIntersectingEventsInRangeToSelectionInWorldSpace( + 9.98, 9.99, 0.1, selection); + assert.strictEqual(selection.length, 1); + assert.instanceOf(tr.b.getOnlyElement(selection), tr.model.ObjectSnapshot); + + // Hit the instance, between the 1st and 2nd snapshots + selection = new EventSet(); + track.addIntersectingEventsInRangeToSelectionInWorldSpace( + 20, 20.1, 0.1, selection); + assert.strictEqual(selection.length, 1); + assert.instanceOf(tr.b.getOnlyElement(selection), tr.model.ObjectInstance); + }); + + test('addEventNearToProvidedEventToSelection', function() { + const objects = createObjects(); + const frames = objects.getAllInstancesByTypeName().Frame; + + const track = ObjectInstanceTrack(new Viewport()); + track.objectInstances = frames; + + const instance = new tr.model.ObjectInstance( + {}, new tr.model.ScopedId('ptr', '0x1000'), 'cat', 'n', 10); + + assert.doesNotThrow(function() { + track.addEventNearToProvidedEventToSelection(instance, 0, undefined); + }); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html new file mode 100644 index 00000000000..e43bce0cec2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/container_track.html"> +<link rel="import" href="/tracing/ui/tracks/spacing_track.html"> +<link rel="import" href="/tracing/ui/tracks/thread_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays threads with only scheduling information but no + * slices. By default it's collapsed to minimize initial visual difference + * while allowing the user to drill-down into whatever process is + * interesting to them. + * @constructor + * @extends {ContainerTrack} + */ + const OtherThreadsTrack = tr.ui.b.define( + 'other-threads-track', tr.ui.tracks.OtherThreadsTrack); + + const SpacingTrack = tr.ui.tracks.SpacingTrack; + + OtherThreadsTrack.prototype = { + + __proto__: tr.ui.tracks.ContainerTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport); + + this.header_ = document.createElement('tr-ui-b-heading'); + this.header_.addEventListener('click', this.onHeaderClick_.bind(this)); + this.header_.heading = 'Other Threads'; + this.header_.tooltip = 'Threads with only scheduling information'; + this.header_.arrowVisible = true; + + this.threads_ = []; + this.expanded = false; + this.collapsible_ = true; + }, + + set threads(threads) { + this.threads_ = threads; + this.updateContents_(); + }, + + set collapsible(collapsible) { + this.collapsible_ = collapsible; + this.updateContents_(); + }, + + onHeaderClick_(e) { + e.stopPropagation(); + e.preventDefault(); + this.expanded = !this.expanded; + }, + + get expanded() { + return this.header_.expanded; + }, + + set expanded(expanded) { + expanded = !!expanded; + + if (this.expanded === expanded) return; + + this.header_.expanded = expanded; + + // Expanding and collapsing tracks is, essentially, growing and shrinking + // the viewport. We dispatch a change event to trigger any processing + // to happen. + this.viewport_.dispatchChangeEvent(); + + this.updateContents_(); + }, + + updateContents_() { + this.detach(); + if (this.collapsible_) { + Polymer.dom(this).appendChild(this.header_); + } + if (this.expanded || !this.collapsible_) { + for (const thread of this.threads_) { + const track = new tr.ui.tracks.ThreadTrack(this.viewport); + track.thread = thread; + if (!track.hasVisibleContent) return; + + Polymer.dom(this).appendChild(track); + Polymer.dom(this).appendChild(new SpacingTrack(this.viewport)); + } + } + } + }; + + return { + OtherThreadsTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html new file mode 100644 index 00000000000..d32cd21e9a3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/chart_point.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series.html"> +<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html"> +<link rel="import" href="/tracing/ui/tracks/chart_track.html"> + +<style> +.power-series-track { + height: 90px; +} +</style> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const ColorScheme = tr.b.ColorScheme; + const ChartTrack = tr.ui.tracks.ChartTrack; + + /** + * A track that displays a PowerSeries. + * + * @constructor + * @extends {ChartTrack} + */ + const PowerSeriesTrack = tr.ui.b.define('power-series-track', ChartTrack); + + PowerSeriesTrack.prototype = { + __proto__: ChartTrack.prototype, + + decorate(viewport) { + ChartTrack.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('power-series-track'); + this.heading = 'Power'; + this.powerSeries_ = undefined; + }, + + set powerSeries(powerSeries) { + this.powerSeries_ = powerSeries; + + this.series = this.buildChartSeries_(); + this.autoSetAllAxes({expandMax: true}); + }, + + get hasVisibleContent() { + return (this.powerSeries_ && this.powerSeries_.samples.length > 0); + }, + + addContainersToTrackMap(containerToTrackMap) { + containerToTrackMap.addContainer(this.powerSeries_, this); + }, + + buildChartSeries_() { + if (!this.hasVisibleContent) return []; + + const seriesYAxis = new tr.ui.tracks.ChartSeriesYAxis(0, undefined); + const pts = this.powerSeries_.samples.map(function(smpl) { + return new tr.ui.tracks.ChartPoint(smpl, smpl.start, smpl.powerInW); + }); + const renderingConfig = { + chartType: tr.ui.tracks.ChartSeriesType.AREA, + colorId: ColorScheme.getColorIdForGeneralPurposeString(this.heading) + }; + + return [new tr.ui.tracks.ChartSeries(pts, seriesYAxis, renderingConfig)]; + } + }; + + return { + PowerSeriesTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html new file mode 100644 index 00000000000..9e8b03aa168 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html @@ -0,0 +1,121 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel='import' href='/tracing/model/device.html'> +<link rel='import' href='/tracing/model/model.html'> +<link rel='import' href='/tracing/model/power_series.html'> +<link rel='import' href='/tracing/ui/base/constants.html'> +<link rel='import' href='/tracing/ui/timeline_viewport.html'> +<link rel='import' href='/tracing/ui/tracks/container_to_track_map.html'> +<link rel='import' href='/tracing/ui/tracks/drawing_container.html'> +<link rel="import" href="/tracing/ui/tracks/power_series_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Device = tr.model.Device; + const Model = tr.Model; + const PowerSeries = tr.model.PowerSeries; + const PowerSeriesTrack = tr.ui.tracks.PowerSeriesTrack; + + const createDrawingContainer = function(series) { + const div = document.createElement('div'); + const viewport = new tr.ui.TimelineViewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + if (series) { + series.updateBounds(); + setDisplayTransformFromBounds(viewport, series.bounds); + } + + return drawingContainer; + }; + + /** + * Sets the mapping between the input range of timestamps and the output range + * of horizontal pixels. + */ + const setDisplayTransformFromBounds = function(viewport, bounds) { + const dt = new tr.ui.TimelineDisplayTransform(); + const pixelRatio = window.devicePixelRatio || 1; + const chartPixelWidth = + (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio; + dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth); + viewport.setDisplayTransformImmediately(dt); + }; + + test('instantiate', function() { + const series = new PowerSeries(new Model().device); + series.addPowerSample(0, 1); + series.addPowerSample(0.5, 2); + series.addPowerSample(1, 3); + series.addPowerSample(1.5, 4); + + const drawingContainer = createDrawingContainer(series); + const track = new PowerSeriesTrack(drawingContainer.viewport); + track.powerSeries = series; + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(drawingContainer); + }); + + test('hasVisibleContent_trueWithPowerSamplesPresent', function() { + const series = new PowerSeries(new Model().device); + series.addPowerSample(0, 1); + series.addPowerSample(0.5, 2); + series.addPowerSample(1, 3); + series.addPowerSample(1.5, 4); + + const div = document.createElement('div'); + const viewport = new tr.ui.TimelineViewport(div); + + const track = new PowerSeriesTrack(viewport); + track.powerSeries = series; + + assert.isTrue(track.hasVisibleContent); + }); + + test('hasVisibleContent_falseWithUndefinedPowerSeries', function() { + const div = document.createElement('div'); + const viewport = new tr.ui.TimelineViewport(div); + + const track = new PowerSeriesTrack(viewport); + track.powerSeries = undefined; + + assert.notOk(track.hasVisibleContent); + }); + + test('hasVisibleContent_falseWithEmptyPowerSeries', function() { + const div = document.createElement('div'); + const viewport = new tr.ui.TimelineViewport(div); + + const track = new PowerSeriesTrack(viewport); + const series = new PowerSeries(new Model().device); + track.powerSeries = series; + + assert.notOk(track.hasVisibleContent); + }); + + test('addContainersToTrackMap', function() { + const div = document.createElement('div'); + const viewport = new tr.ui.TimelineViewport(div); + + const powerSeriesTrack = new PowerSeriesTrack(viewport); + const series = new PowerSeries(new Model().device); + powerSeriesTrack.powerSeries = series; + + const containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap(); + powerSeriesTrack.addContainersToTrackMap(containerToTrackMap); + + assert.strictEqual( + containerToTrackMap.getTrackByStableId('Device.PowerSeries'), + powerSeriesTrack); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html new file mode 100644 index 00000000000..247d707f58f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/tracks/chart_track.html"> +<link rel="import" href="/tracing/ui/tracks/container_track.html"> +<link rel="import" href="/tracing/ui/tracks/memory_dump_track_util.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const ALLOCATED_MEMORY_TRACK_HEIGHT = 50; + + /** + * A track that displays an array of ProcessMemoryDump objects. + * @constructor + * @extends {ContainerTrack} + */ + const ProcessMemoryDumpTrack = tr.ui.b.define( + 'process-memory-dump-track', tr.ui.tracks.ContainerTrack); + + ProcessMemoryDumpTrack.prototype = { + __proto__: tr.ui.tracks.ContainerTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport); + this.memoryDumps_ = undefined; + }, + + get memoryDumps() { + return this.memoryDumps_; + }, + + set memoryDumps(memoryDumps) { + this.memoryDumps_ = memoryDumps; + this.updateContents_(); + }, + + updateContents_() { + this.clearTracks_(); + + // Show no tracks if there are no dumps. + if (!this.memoryDumps_ || !this.memoryDumps_.length) return; + + this.appendAllocatedMemoryTrack_(); + }, + + appendAllocatedMemoryTrack_() { + const series = tr.ui.tracks.buildProcessAllocatedMemoryChartSeries( + this.memoryDumps_); + if (!series) return; + + const track = new tr.ui.tracks.ChartTrack(this.viewport); + track.heading = 'Memory per component'; + track.height = ALLOCATED_MEMORY_TRACK_HEIGHT + 'px'; + track.series = series; + track.autoSetAllAxes({expandMax: true}); + Polymer.dom(this).appendChild(track); + } + }; + + return { + ProcessMemoryDumpTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html new file mode 100644 index 00000000000..897d2883c62 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> +<link rel="import" href="/tracing/ui/tracks/memory_dump_track_test_utils.html"> +<link rel="import" href="/tracing/ui/tracks/process_memory_dump_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Viewport = tr.ui.TimelineViewport; + const ProcessMemoryDumpTrack = tr.ui.tracks.ProcessMemoryDumpTrack; + const createTestProcessMemoryDumps = + tr.ui.tracks.createTestProcessMemoryDumps; + + function instantiateTrack(withVMRegions, withAllocatorDumps, + expectedTrackCount) { + const dumps = createTestProcessMemoryDumps( + withVMRegions, withAllocatorDumps); + + const div = document.createElement('div'); + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = new ProcessMemoryDumpTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + drawingContainer.invalidate(); + + track.memoryDumps = dumps; + + // TODO(petrcermak): Check that the div has indeed zero size. + if (expectedTrackCount > 0) { + this.addHTMLOutput(div); + } + + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 50, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + + assert.lengthOf(track.tracks_, expectedTrackCount); + } + + test('instantiate_withoutMemoryAllocatorDumps', function() { + instantiateTrack.call(this, false, false, 0); + }); + test('instantiate_withMemoryAllocatorDumps', function() { + instantiateTrack.call(this, false, true, 1); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html new file mode 100644 index 00000000000..c6560f40118 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html @@ -0,0 +1,130 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/rect_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const ColorScheme = tr.b.ColorScheme; + + /** + * Visualizes a Process's state using a series of rects to represent activity. + * @constructor + */ + const ProcessSummaryTrack = tr.ui.b.define('process-summary-track', + tr.ui.tracks.RectTrack); + + ProcessSummaryTrack.buildRectsFromProcess = function(process) { + if (!process) return []; + + const ops = []; + // build list of start/end ops for each top level or important slice + const pushOp = function(isStart, time, slice) { + ops.push({ + isStart, + time, + slice + }); + }; + for (const tid in process.threads) { + const sliceGroup = process.threads[tid].sliceGroup; + + sliceGroup.topLevelSlices.forEach(function(slice) { + pushOp(true, slice.start, undefined); + pushOp(false, slice.end, undefined); + }); + sliceGroup.slices.forEach(function(slice) { + if (slice.important) { + pushOp(true, slice.start, slice); + pushOp(false, slice.end, slice); + } + }); + } + ops.sort(function(a, b) { return a.time - b.time; }); + + const rects = []; + /** + * Build a row of rects which display one way for unimportant activity, + * and during important slices, show up as those important slices. + * + * If an important slice starts in the middle of another, + * just drop it on the floor. + */ + const genericColorId = ColorScheme.getColorIdForReservedName( + 'generic_work'); + const pushRect = function(start, end, slice) { + rects.push(new tr.ui.tracks.Rect( + slice, /* modelItem: show selection state of slice if present */ + slice ? slice.title : '', /* title */ + slice ? slice.colorId : genericColorId, /* colorId */ + start, /* start */ + end - start /* duration */)); + }; + let depth = 0; + let currentSlice = undefined; + let lastStart = undefined; + ops.forEach(function(op) { + depth += op.isStart ? 1 : -1; + + if (currentSlice) { + // simply find end of current important slice + if (!op.isStart && op.slice === currentSlice) { + // important slice has ended + pushRect(lastStart, op.time, currentSlice); + lastStart = depth >= 1 ? op.time : undefined; + currentSlice = undefined; + } + } else { + if (op.isStart) { + if (depth === 1) { + lastStart = op.time; + currentSlice = op.slice; + } else if (op.slice) { + // switch to slice + if (op.time !== lastStart) { + pushRect(lastStart, op.time, undefined); + lastStart = op.time; + } + currentSlice = op.slice; + } + } else { + if (depth === 0) { + pushRect(lastStart, op.time, undefined); + lastStart = undefined; + } + } + } + }); + return rects; + }; + + ProcessSummaryTrack.prototype = { + __proto__: tr.ui.tracks.RectTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.RectTrack.prototype.decorate.call(this, viewport); + }, + + get process() { + return this.process_; + }, + + set process(process) { + this.process_ = process; + this.rects = ProcessSummaryTrack.buildRectsFromProcess(process); + } + }; + + return { + ProcessSummaryTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html new file mode 100644 index 00000000000..1d071f9d0ce --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/slice_group.html"> +<link rel="import" href="/tracing/ui/tracks/process_summary_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ProcessSummaryTrack = tr.ui.tracks.ProcessSummaryTrack; + + test('buildRectSimple', function() { + let process; + const model = tr.c.TestUtils.newModel(function(model) { + process = model.getOrCreateProcess(1); + // XXXX + // XXXX + const thread1 = process.getOrCreateThread(1); + thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + {start: 1, duration: 4})); + const thread2 = process.getOrCreateThread(2); + thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + {start: 4, duration: 4})); + }); + + const rects = ProcessSummaryTrack.buildRectsFromProcess(process); + + assert.strictEqual(rects.length, 1); + const rect = rects[0]; + assert.closeTo(rect.start, 1, 1e-5); + assert.closeTo(rect.end, 8, 1e-5); + }); + + test('buildRectComplex', function() { + let process; + const model = tr.c.TestUtils.newModel(function(model) { + process = model.getOrCreateProcess(1); + // XXXX X X XX + // XXXX XXX X + const thread1 = process.getOrCreateThread(1); + thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + {start: 1, duration: 4})); + thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + {start: 9, duration: 1})); + thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + {start: 11, duration: 1})); + thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + {start: 13, duration: 2})); + const thread2 = process.getOrCreateThread(2); + thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + {start: 4, duration: 4})); + thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + {start: 9, duration: 3})); + thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + {start: 16, duration: 1})); + }); + + const rects = ProcessSummaryTrack.buildRectsFromProcess(process); + + assert.strictEqual(4, rects.length); + assert.closeTo(rects[0].start, 1, 1e-5); + assert.closeTo(rects[0].end, 8, 1e-5); + assert.closeTo(rects[1].start, 9, 1e-5); + assert.closeTo(rects[1].end, 12, 1e-5); + assert.closeTo(rects[2].start, 13, 1e-5); + assert.closeTo(rects[2].end, 15, 1e-5); + assert.closeTo(rects[3].start, 16, 1e-5); + assert.closeTo(rects[3].end, 17, 1e-5); + }); + + test('buildRectImportantSlice', function() { + let process; + const model = tr.c.TestUtils.newModel(function(model) { + // [ unimportant ] + // [important] + const a = tr.c.TestUtils.newSliceEx( + {title: 'unimportant', start: 4, duration: 21}); + const b = tr.c.TestUtils.newSliceEx( + {title: 'important', start: 9, duration: 11}); + b.important = true; + process = model.getOrCreateProcess(1); + process.getOrCreateThread(1).sliceGroup.pushSlices([a, b]); + + model.importantSlice = b; + }); + + const rects = ProcessSummaryTrack.buildRectsFromProcess(process); + + assert.strictEqual(3, rects.length); + assert.closeTo(rects[0].start, 4, 1e-5); + assert.closeTo(rects[0].end, 9, 1e-5); + assert.closeTo(rects[1].start, 9, 1e-5); + assert.closeTo(rects[1].end, 20, 1e-5); + assert.closeTo(rects[2].start, 20, 1e-5); + assert.closeTo(rects[2].end, 25, 1e-5); + + // middle rect represents important slice, so colorId & title are preserved + assert.strictEqual(rects[1].title, model.importantSlice.title); + assert.strictEqual(rects[1].colorId, model.importantSlice.colorId); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html new file mode 100644 index 00000000000..1be51cbf4cb --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html @@ -0,0 +1,155 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/draw_helpers.html"> +<link rel="import" href="/tracing/ui/tracks/process_memory_dump_track.html"> +<link rel="import" href="/tracing/ui/tracks/process_track_base.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const ProcessTrackBase = tr.ui.tracks.ProcessTrackBase; + + /** + * @constructor + */ + const ProcessTrack = tr.ui.b.define('process-track', ProcessTrackBase); + + ProcessTrack.prototype = { + __proto__: ProcessTrackBase.prototype, + + decorate(viewport) { + tr.ui.tracks.ProcessTrackBase.prototype.decorate.call(this, viewport); + }, + + drawTrack(type) { + switch (type) { + case tr.ui.tracks.DrawType.INSTANT_EVENT: { + if (!this.processBase.instantEvents || + this.processBase.instantEvents.length === 0) { + break; + } + + const ctx = this.context(); + + const pixelRatio = window.devicePixelRatio || 1; + const bounds = this.getBoundingClientRect(); + const canvasBounds = ctx.canvas.getBoundingClientRect(); + + ctx.save(); + ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top)); + + const dt = this.viewport.currentDisplayTransform; + const viewLWorld = dt.xViewToWorld(0); + const viewRWorld = dt.xViewToWorld(canvasBounds.width * pixelRatio); + + tr.ui.b.drawInstantSlicesAsLines( + ctx, + this.viewport.currentDisplayTransform, + viewLWorld, + viewRWorld, + bounds.height, + this.processBase.instantEvents, + 2); + + ctx.restore(); + + break; + } + + case tr.ui.tracks.DrawType.BACKGROUND: + this.drawBackground_(); + // Don't bother recursing further, Process is the only level that + // draws backgrounds. + return; + } + + tr.ui.tracks.ContainerTrack.prototype.drawTrack.call(this, type); + }, + + drawBackground_() { + const ctx = this.context(); + const canvasBounds = ctx.canvas.getBoundingClientRect(); + const pixelRatio = window.devicePixelRatio || 1; + + let draw = false; + ctx.fillStyle = '#eee'; + for (let i = 0; i < this.children.length; ++i) { + if (!(this.children[i] instanceof tr.ui.tracks.Track) || + (this.children[i] instanceof tr.ui.tracks.SpacingTrack)) { + continue; + } + + draw = !draw; + if (!draw) continue; + + const bounds = this.children[i].getBoundingClientRect(); + ctx.fillRect(0, pixelRatio * (bounds.top - canvasBounds.top), + ctx.canvas.width, pixelRatio * bounds.height); + } + }, + + // Process maps to processBase because we derive from ProcessTrackBase. + set process(process) { + this.processBase = process; + }, + + get process() { + return this.processBase; + }, + + get eventContainer() { + return this.process; + }, + + addContainersToTrackMap(containerToTrackMap) { + tr.ui.tracks.ProcessTrackBase.prototype.addContainersToTrackMap.apply( + this, arguments); + containerToTrackMap.addContainer(this.process, this); + }, + + appendMemoryDumpTrack_() { + const processMemoryDumps = this.process.memoryDumps; + if (processMemoryDumps.length) { + const pmdt = new tr.ui.tracks.ProcessMemoryDumpTrack(this.viewport_); + pmdt.memoryDumps = processMemoryDumps; + Polymer.dom(this).appendChild(pmdt); + } + }, + + addIntersectingEventsInRangeToSelectionInWorldSpace( + loWX, hiWX, viewPixWidthWorld, selection) { + function onPickHit(instantEvent) { + selection.push(instantEvent); + } + const instantEventWidth = 2 * viewPixWidthWorld; + tr.b.iterateOverIntersectingIntervals(this.processBase.instantEvents, + function(x) { return x.start; }, + function(x) { return x.duration + instantEventWidth; }, + loWX, hiWX, + onPickHit.bind(this)); + + tr.ui.tracks.ContainerTrack.prototype. + addIntersectingEventsInRangeToSelectionInWorldSpace. + apply(this, arguments); + }, + + addClosestEventToSelection(worldX, worldMaxDist, loY, hiY, + selection) { + this.addClosestInstantEventToSelection(this.processBase.instantEvents, + worldX, worldMaxDist, selection); + tr.ui.tracks.ContainerTrack.prototype.addClosestEventToSelection. + apply(this, arguments); + } + }; + + return { + ProcessTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css new file mode 100644 index 00000000000..25fa5f015b7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css @@ -0,0 +1,39 @@ +/* Copyright (c) 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +.process-track-header { + display: flex; + flex: 0 0 auto; + background-image: -webkit-gradient(linear, + 0 0, 100% 0, + from(#E5E5E5), + to(#D1D1D1)); + border-bottom: 1px solid #8e8e8e; + border-top: 1px solid white; + font-size: 75%; +} + +.process-track-name { + flex-grow: 1; +} + +.process-track-name:before { + content: '\25B8'; /* Right triangle */ + padding: 0 5px; +} + +.process-track-base.expanded .process-track-name:before { + content: '\25BE'; /* Down triangle */ +} + +.process-track-close { + color: black; + border: 1px solid transparent; + padding: 0px 2px; +} + +.process-track-close:hover { + border: 1px solid grey; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html new file mode 100644 index 00000000000..89358b8411e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html @@ -0,0 +1,313 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="stylesheet" href="/tracing/ui/tracks/process_track_base.css"> + +<link rel="import" href="/tracing/core/filter.html"> +<link rel="import" href="/tracing/model/model_settings.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/container_track.html"> +<link rel="import" href="/tracing/ui/tracks/counter_track.html"> +<link rel="import" href="/tracing/ui/tracks/frame_track.html"> +<link rel="import" href="/tracing/ui/tracks/object_instance_group_track.html"> +<link rel="import" href="/tracing/ui/tracks/other_threads_track.html"> +<link rel="import" href="/tracing/ui/tracks/process_summary_track.html"> +<link rel="import" href="/tracing/ui/tracks/spacing_track.html"> +<link rel="import" href="/tracing/ui/tracks/thread_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + const ObjectSnapshotView = tr.ui.analysis.ObjectSnapshotView; + const ObjectInstanceView = tr.ui.analysis.ObjectInstanceView; + const SpacingTrack = tr.ui.tracks.SpacingTrack; + + /** + * Visualizes a Process by building ThreadTracks and CounterTracks. + * @constructor + */ + const ProcessTrackBase = + tr.ui.b.define('process-track-base', tr.ui.tracks.ContainerTrack); + + ProcessTrackBase.prototype = { + + __proto__: tr.ui.tracks.ContainerTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport); + + this.processBase_ = undefined; + + Polymer.dom(this).classList.add('process-track-base'); + Polymer.dom(this).classList.add('expanded'); + + this.processNameEl_ = tr.ui.b.createSpan(); + Polymer.dom(this.processNameEl_).classList.add('process-track-name'); + + this.closeEl_ = tr.ui.b.createSpan(); + Polymer.dom(this.closeEl_).classList.add('process-track-close'); + this.closeEl_.textContent = 'X'; + + this.headerEl_ = tr.ui.b.createDiv({className: 'process-track-header'}); + Polymer.dom(this.headerEl_).appendChild(this.processNameEl_); + Polymer.dom(this.headerEl_).appendChild(this.closeEl_); + this.headerEl_.addEventListener('click', this.onHeaderClick_.bind(this)); + + Polymer.dom(this).appendChild(this.headerEl_); + }, + + get processBase() { + return this.processBase_; + }, + + set processBase(processBase) { + this.processBase_ = processBase; + + if (this.processBase_) { + const modelSettings = new tr.model.ModelSettings( + this.processBase_.model); + const defaultValue = this.processBase_.important; + this.expanded = modelSettings.getSettingFor( + this.processBase_, 'expanded', defaultValue); + } + + this.updateContents_(); + }, + + get expanded() { + return Polymer.dom(this).classList.contains('expanded'); + }, + + set expanded(expanded) { + expanded = !!expanded; + + if (this.expanded === expanded) return; + + Polymer.dom(this).classList.toggle('expanded'); + + // Expanding and collapsing tracks is, essentially, growing and shrinking + // the viewport. We dispatch a change event to trigger any processing + // to happen. + this.viewport_.dispatchChangeEvent(); + + if (!this.processBase_) return; + + const modelSettings = new tr.model.ModelSettings(this.processBase_.model); + modelSettings.setSettingFor(this.processBase_, 'expanded', expanded); + this.updateContents_(); + this.viewport.rebuildEventToTrackMap(); + this.viewport.rebuildContainerToTrackMap(); + }, + + set visible(visible) { + if (visible === this.visible) return; + this.hidden = !visible; + + tr.b.dispatchSimpleEvent(this, 'visibility'); + // Changing the visibility of the tracks can grow and shrink the viewport. + // We dispatch a change event to trigger any processing to happen. + this.viewport_.dispatchChangeEvent(); + + if (!this.processBase_) return; + + this.updateContents_(); + this.viewport.rebuildEventToTrackMap(); + this.viewport.rebuildContainerToTrackMap(); + }, + + get visible() { + return !this.hidden; + }, + + get hasVisibleContent() { + if (this.expanded) { + return this.children.length > 1; + } + return true; + }, + + onHeaderClick_(e) { + e.stopPropagation(); + e.preventDefault(); + if (e.target === this.closeEl_) { + this.visible = false; + } else { + this.expanded = !this.expanded; + } + }, + + updateContents_() { + this.clearTracks_(); + + if (!this.processBase_) return; + + Polymer.dom(this.processNameEl_).textContent = + this.processBase_.userFriendlyName; + this.headerEl_.title = this.processBase_.userFriendlyDetails; + + // Create the object instance tracks for this process. + this.willAppendTracks_(); + if (this.expanded) { + this.appendMemoryDumpTrack_(); + this.appendObjectInstanceTracks_(); + this.appendCounterTracks_(); + this.appendFrameTrack_(); + this.appendThreadTracks_(); + } else { + this.appendSummaryTrack_(); + } + this.didAppendTracks_(); + }, + + willAppendTracks_() { + }, + + didAppendTracks_() { + }, + + appendMemoryDumpTrack_() { + }, + + appendSummaryTrack_() { + const track = new tr.ui.tracks.ProcessSummaryTrack(this.viewport); + track.process = this.process; + if (!track.hasVisibleContent) return; + Polymer.dom(this).appendChild(track); + // no spacing track, since this track only shown in collapsed state + }, + + appendFrameTrack_() { + const frames = this.process ? this.process.frames : undefined; + if (!frames || !frames.length) return; + + const track = new tr.ui.tracks.FrameTrack(this.viewport); + track.frames = frames; + Polymer.dom(this).appendChild(track); + }, + + appendObjectInstanceTracks_() { + const instancesByTypeName = + this.processBase_.objects.getAllInstancesByTypeName(); + const instanceTypeNames = Object.keys(instancesByTypeName); + instanceTypeNames.sort(); + + let didAppendAtLeastOneTrack = false; + instanceTypeNames.forEach(function(typeName) { + const allInstances = instancesByTypeName[typeName]; + + // If a object snapshot has a view it will be shown, + // unless the view asked for it to not be shown. + let instanceViewInfo = ObjectInstanceView.getTypeInfo( + undefined, typeName); + let snapshotViewInfo = ObjectSnapshotView.getTypeInfo( + undefined, typeName); + if (instanceViewInfo && !instanceViewInfo.metadata.showInTrackView) { + instanceViewInfo = undefined; + } + if (snapshotViewInfo && !snapshotViewInfo.metadata.showInTrackView) { + snapshotViewInfo = undefined; + } + const hasViewInfo = instanceViewInfo || snapshotViewInfo; + + // There are some instances that don't merit their own track in + // the UI. Filter them out. + const visibleInstances = []; + for (let i = 0; i < allInstances.length; i++) { + const instance = allInstances[i]; + + // Do not create tracks for instances that have no snapshots. + if (instance.snapshots.length === 0) continue; + + // Do not create tracks for instances that have implicit snapshots + // and don't have a view. + if (instance.hasImplicitSnapshots && !hasViewInfo) continue; + + visibleInstances.push(instance); + } + if (visibleInstances.length === 0) return; + + // Look up the constructor for this track, or use the default + // constructor if none exists. + let trackConstructor = + tr.ui.tracks.ObjectInstanceTrack.getConstructor( + undefined, typeName); + if (!trackConstructor) { + snapshotViewInfo = ObjectSnapshotView.getTypeInfo( + undefined, typeName); + if (snapshotViewInfo && snapshotViewInfo.metadata.showInstances) { + trackConstructor = tr.ui.tracks.ObjectInstanceGroupTrack; + } else { + trackConstructor = tr.ui.tracks.ObjectInstanceTrack; + } + } + const track = new trackConstructor(this.viewport); + track.objectInstances = visibleInstances; + Polymer.dom(this).appendChild(track); + didAppendAtLeastOneTrack = true; + }, this); + if (didAppendAtLeastOneTrack) { + Polymer.dom(this).appendChild(new SpacingTrack(this.viewport)); + } + }, + + appendCounterTracks_() { + // Add counter tracks for this process. + const counters = Object.values(this.processBase.counters); + counters.sort(tr.model.Counter.compare); + + // Create the counters for this process. + counters.forEach(function(counter) { + const track = new tr.ui.tracks.CounterTrack(this.viewport); + track.counter = counter; + Polymer.dom(this).appendChild(track); + Polymer.dom(this).appendChild(new SpacingTrack(this.viewport)); + }.bind(this)); + }, + + appendThreadTracks_() { + // Get a sorted list of threads. + const threads = Object.values(this.processBase.threads); + threads.sort(tr.model.Thread.compare); + + // Create the threads. + const otherThreads = []; + let hasVisibleThreads = false; + threads.forEach(function(thread) { + const track = new tr.ui.tracks.ThreadTrack(this.viewport); + track.thread = thread; + if (!track.hasVisibleContent) return; + + if (track.hasSlices) { + hasVisibleThreads = true; + Polymer.dom(this).appendChild(track); + Polymer.dom(this).appendChild(new SpacingTrack(this.viewport)); + } else if (track.hasTimeSlices) { + otherThreads.push(thread); + } + }.bind(this)); + + if (otherThreads.length > 0) { + // If there's only 1 thread with scheduling-only information don't + // bother making a group, just display it directly + // Similarly if we are a process with only scheduling-only threads + // don't bother making a group as the process itself serves + // as the collapsable group + const track = new tr.ui.tracks.OtherThreadsTrack(this.viewport); + track.threads = otherThreads; + track.collapsible = otherThreads.length > 1 && hasVisibleThreads; + Polymer.dom(this).appendChild(track); + } + } + }; + + return { + ProcessTrackBase, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css new file mode 100644 index 00000000000..0467c91562c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css @@ -0,0 +1,8 @@ +/* Copyright (c) 2014 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +.rect-track { + height: 18px; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html new file mode 100644 index 00000000000..65e073d32ee --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html @@ -0,0 +1,249 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="stylesheet" href="/tracing/ui/tracks/rect_track.css"> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/proxy_selectable_item.html"> +<link rel="import" href="/tracing/ui/base/draw_helpers.html"> +<link rel="import" href="/tracing/ui/base/fast_rect_renderer.html"> +<link rel="import" href="/tracing/ui/base/heading.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays an array of Rect objects. + * @constructor + * @extends {Track} + */ + const RectTrack = tr.ui.b.define( + 'rect-track', tr.ui.tracks.Track); + + RectTrack.prototype = { + + __proto__: tr.ui.tracks.Track.prototype, + + decorate(viewport) { + tr.ui.tracks.Track.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('rect-track'); + this.asyncStyle_ = false; + this.rects_ = null; + + this.heading_ = document.createElement('tr-ui-b-heading'); + Polymer.dom(this).appendChild(this.heading_); + }, + + set heading(heading) { + this.heading_.heading = heading; + }, + + get heading() { + return this.heading_.heading; + }, + + set tooltip(tooltip) { + this.heading_.tooltip = tooltip; + }, + + set selectionGenerator(generator) { + this.heading_.selectionGenerator = generator; + }, + + set expanded(expanded) { + this.heading_.expanded = !!expanded; + }, + + set arrowVisible(arrowVisible) { + this.heading_.arrowVisible = !!arrowVisible; + }, + + get expanded() { + return this.heading_.expanded; + }, + + get asyncStyle() { + return this.asyncStyle_; + }, + + set asyncStyle(v) { + this.asyncStyle_ = !!v; + }, + + get rects() { + return this.rects_; + }, + + set rects(rects) { + this.rects_ = rects || []; + this.invalidateDrawingContainer(); + }, + + get height() { + return window.getComputedStyle(this).height; + }, + + set height(height) { + this.style.height = height; + this.invalidateDrawingContainer(); + }, + + get hasVisibleContent() { + return this.rects_.length > 0; + }, + + draw(type, viewLWorld, viewRWorld, viewHeight) { + switch (type) { + case tr.ui.tracks.DrawType.GENERAL_EVENT: + this.drawRects_(viewLWorld, viewRWorld); + break; + } + }, + + drawRects_(viewLWorld, viewRWorld) { + const ctx = this.context(); + + ctx.save(); + const bounds = this.getBoundingClientRect(); + tr.ui.b.drawSlices( + ctx, + this.viewport.currentDisplayTransform, + viewLWorld, + viewRWorld, + bounds.height, + this.rects_, + this.asyncStyle_); + ctx.restore(); + + if (bounds.height <= 6) return; + + let fontSize; + let yOffset; + if (bounds.height < 15) { + fontSize = 6; + yOffset = 1.0; + } else { + fontSize = 10; + yOffset = 2.5; + } + tr.ui.b.drawLabels( + ctx, + this.viewport.currentDisplayTransform, + viewLWorld, + viewRWorld, + this.rects_, + this.asyncStyle_, + fontSize, + yOffset); + }, + + addEventsToTrackMap(eventToTrackMap) { + if (this.rects_ === undefined || this.rects_ === null) { + return; + } + + this.rects_.forEach(function(rect) { + rect.addToTrackMap(eventToTrackMap, this); + }, this); + }, + + addIntersectingEventsInRangeToSelectionInWorldSpace( + loWX, hiWX, viewPixWidthWorld, selection) { + function onRect(rect) { + rect.addToSelection(selection); + } + onRect = onRect.bind(this); + const instantEventWidth = 2 * viewPixWidthWorld; + tr.b.iterateOverIntersectingIntervals(this.rects_, + function(x) { return x.start; }, + function(x) { + return x.duration === 0 ? + x.duration + instantEventWidth : + x.duration; + }, + loWX, hiWX, + onRect); + }, + + /** + * Add the item to the left or right of the provided event, if any, to the + * selection. + * @param {rect} The current rect. + * @param {Number} offset Number of rects away from the event to look. + * @param {Selection} selection The selection to add an event to, + * if found. + * @return {boolean} Whether an event was found. + * @private + */ + addEventNearToProvidedEventToSelection(event, offset, selection) { + const index = this.rects_.findIndex(rect => rect.modelItem === event); + if (index === -1) return false; + + const newIndex = index + offset; + if (newIndex < 0 || newIndex >= this.rects_.length) return false; + + this.rects_[newIndex].addToSelection(selection); + return true; + }, + + addAllEventsMatchingFilterToSelection(filter, selection) { + for (let i = 0; i < this.rects_.length; ++i) { + // TODO(petrcermak): Rather than unpacking the proxy item here, + // we should probably add an addToSelectionIfMatching(selection, filter) + // method to SelectableItem (#900). + const modelItem = this.rects_[i].modelItem; + if (!modelItem) continue; + + if (filter.matchSlice(modelItem)) { + selection.push(modelItem); + } + } + }, + + addClosestEventToSelection(worldX, worldMaxDist, loY, hiY, + selection) { + const rect = tr.b.findClosestIntervalInSortedIntervals( + this.rects_, + function(x) { return x.start; }, + function(x) { return x.end; }, + worldX, + worldMaxDist); + + if (!rect) return; + + rect.addToSelection(selection); + } + }; + + /** + * A filled rectangle with a title. + * + * @constructor + * @extends {ProxySelectableItem} + */ + function Rect(modelItem, title, colorId, start, duration) { + tr.model.ProxySelectableItem.call(this, modelItem); + this.title = title; + this.colorId = colorId; + this.start = start; + this.duration = duration; + this.end = start + duration; + } + + Rect.prototype = { + __proto__: tr.model.ProxySelectableItem.prototype + }; + + return { + RectTrack, + Rect, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html new file mode 100644 index 00000000000..ec81a8835d7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html @@ -0,0 +1,412 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/slice.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/draw_helpers.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const EventSet = tr.model.EventSet; + const RectTrack = tr.ui.tracks.RectTrack; + const Rect = tr.ui.tracks.Rect; + const ThreadSlice = tr.model.ThreadSlice; + const Viewport = tr.ui.TimelineViewport; + + test('instantiate_withRects', function() { + const div = document.createElement('div'); + + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = RectTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + track.heading = 'testBasicRects'; + track.rects = [ + new Rect(undefined, 'a', 0, 1, 1), + new Rect(undefined, 'b', 1, 2.1, 4.8), + new Rect(undefined, 'b', 1, 7, 0.5), + new Rect(undefined, 'c', 2, 7.6, 0.4) + ]; + + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 8.8, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + }); + + test('instantiate_withSlices', function() { + const div = document.createElement('div'); + + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = RectTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + track.heading = 'testBasicSlices'; + track.rects = [ + new ThreadSlice('', 'a', 0, 1, {}, 1), + new ThreadSlice('', 'b', 1, 2.1, {}, 4.8), + new ThreadSlice('', 'b', 1, 7, {}, 0.5), + new ThreadSlice('', 'c', 2, 7.6, {}, 0.4) + ]; + + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 8.8, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + }); + + test('instantiate_shrinkingRectSize', function() { + const div = document.createElement('div'); + + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = RectTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + track.heading = 'testShrinkingRectSizes'; + let x = 0; + const widths = [10, 5, 4, 3, 2, 1, 0.5, 0.4, 0.3, 0.2, 0.1, 0.05]; + const slices = []; + for (let i = 0; i < widths.length; i++) { + const s = new Rect(undefined, 'a', 1, x, widths[i]); + x += s.duration + 0.5; + slices.push(s); + } + track.rects = slices; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 1.1 * x, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + }); + + test('instantiate_elide', function() { + const optDicts = [{ trackName: 'elideOff', elide: false }, + { trackName: 'elideOn', elide: true }]; + + const tooLongTitle = 'Unless eliding this SHOULD NOT BE DISPLAYED. '; + const bigTitle = 'Very big title name that goes on longer ' + + 'than you may expect'; + + for (const dictIndex in optDicts) { + const dict = optDicts[dictIndex]; + + const div = document.createElement('div'); + Polymer.dom(div).appendChild(document.createTextNode(dict.trackName)); + + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = new RectTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + track.SHOULD_ELIDE_TEXT = dict.elide; + track.heading = 'Visual: ' + dict.trackName; + track.rects = [ + // title, colorId, start, args, opt_duration + new Rect(undefined, 'a ' + tooLongTitle + bigTitle, 0, 1, 1), + new Rect(undefined, bigTitle, 1, 2.1, 4.8), + new Rect(undefined, 'cccc cccc cccc', 1, 7, 0.5), + new Rect(undefined, 'd', 2, 7.6, 1.0) + ]; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 9.5, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + } + }); + + test('findAllObjectsMatchingInRectTrack', function() { + const track = new RectTrack(new tr.ui.TimelineViewport()); + track.rects = [ + new ThreadSlice('', 'a', 0, 1, {}, 1), + new ThreadSlice('', 'b', 1, 2.1, {}, 4.8), + new ThreadSlice('', 'b', 1, 7, {}, 0.5), + new ThreadSlice('', 'c', 2, 7.6, {}, 0.4) + ]; + const selection = new EventSet(); + track.addAllEventsMatchingFilterToSelection( + new tr.c.TitleOrCategoryFilter('b'), selection); + + const predictedSelection = new EventSet( + [track.rects[1].modelItem, track.rects[2].modelItem]); + assert.isTrue(selection.equals(predictedSelection)); + }); + + test('selectionHitTesting', function() { + const testEl = document.createElement('div'); + Polymer.dom(testEl).appendChild( + tr.ui.b.createScopedStyle('heading { width: 100px; }')); + testEl.style.width = '600px'; + + const viewport = new Viewport(testEl); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(testEl).appendChild(drawingContainer); + + const track = new RectTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + this.addHTMLOutput(testEl); + + drawingContainer.updateCanvasSizeIfNeeded_(); + + track.heading = 'testSelectionHitTesting'; + track.rects = [ + new ThreadSlice('', 'a', 0, 1, {}, 1), + new ThreadSlice('', 'b', 1, 5, {}, 4.8) + ]; + const y = track.getBoundingClientRect().top + 5; + const pixelRatio = window.devicePixelRatio || 1; + const wW = 10; + const vW = drawingContainer.canvas.getBoundingClientRect().width; + + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, wW, vW * pixelRatio); + track.viewport.setDisplayTransformImmediately(dt); + + let selection = new EventSet(); + let x = (1.5 / wW) * vW; + track.addIntersectingEventsInRangeToSelection( + x, x + 1, y, y + 1, selection); + assert.isTrue(selection.equals(new EventSet(track.rects[0].modelItem))); + + selection = new EventSet(); + x = (2.1 / wW) * vW; + track.addIntersectingEventsInRangeToSelection( + x, x + 1, y, y + 1, selection); + assert.strictEqual(0, selection.length); + + selection = new EventSet(); + x = (6.8 / wW) * vW; + track.addIntersectingEventsInRangeToSelection( + x, x + 1, y, y + 1, selection); + assert.isTrue(selection.equals(new EventSet(track.rects[1].modelItem))); + + selection = new EventSet(); + x = (9.9 / wW) * vW; + track.addIntersectingEventsInRangeToSelection( + x, x + 1, y, y + 1, selection); + assert.strictEqual(0, selection.length); + }); + + test('elide', function() { + const testEl = document.createElement('div'); + + const viewport = new Viewport(testEl); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(testEl).appendChild(drawingContainer); + + const track = new RectTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + this.addHTMLOutput(testEl); + + drawingContainer.updateCanvasSizeIfNeeded_(); + + const bigtitle = 'Super duper long long title ' + + 'holy moly when did you get so verbose?'; + const smalltitle = 'small'; + track.heading = 'testElide'; + track.rects = [ + // title, colorId, start, args, opt_duration + new ThreadSlice('', bigtitle, 0, 1, {}, 1), + new ThreadSlice('', smalltitle, 1, 2, {}, 1) + ]; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 3.3, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + + let stringWidthPair = undefined; + const pixWidth = dt.xViewVectorToWorld(1); + + // Small titles on big slices are not elided. + stringWidthPair = + tr.ui.b.elidedTitleCache_.get( + track.context(), + pixWidth, + smalltitle, + tr.ui.b.elidedTitleCache_.labelWidth( + track.context(), + smalltitle), + 1); + assert.strictEqual(smalltitle, stringWidthPair.string); + + // Keep shrinking the slice until eliding starts. + let elidedWhenSmallEnough = false; + for (let sliceLength = 1; sliceLength >= 0.00001; sliceLength /= 2.0) { + stringWidthPair = + tr.ui.b.elidedTitleCache_.get( + track.context(), + pixWidth, + smalltitle, + tr.ui.b.elidedTitleCache_.labelWidth( + track.context(), + smalltitle), + sliceLength); + if (stringWidthPair.string.length < smalltitle.length) { + elidedWhenSmallEnough = true; + break; + } + } + assert.isTrue(elidedWhenSmallEnough); + + // Big titles are elided immediately. + let superBigTitle = ''; + for (let x = 0; x < 10; x++) { + superBigTitle += bigtitle; + } + stringWidthPair = + tr.ui.b.elidedTitleCache_.get( + track.context(), + pixWidth, + superBigTitle, + tr.ui.b.elidedTitleCache_.labelWidth( + track.context(), + superBigTitle), + 1); + assert.isTrue(stringWidthPair.string.length < superBigTitle.length); + + // And elided text ends with ... + const len = stringWidthPair.string.length; + assert.strictEqual('...', stringWidthPair.string.substring(len - 3, len)); + }); + + test('rectTrackAddItemNearToProvidedEvent', function() { + const track = new RectTrack(new tr.ui.TimelineViewport()); + track.rects = [ + new ThreadSlice('', 'a', 0, 1, {}, 1), + new ThreadSlice('', 'b', 1, 2.1, {}, 4.8), + new ThreadSlice('', 'b', 1, 7, {}, 0.5), + new ThreadSlice('', 'c', 2, 7.6, {}, 0.4) + ]; + let sel = new EventSet(); + track.addAllEventsMatchingFilterToSelection( + new tr.c.TitleOrCategoryFilter('b'), sel); + + // Select to the right of B. + const selRight = new EventSet(); + let ret = track.addEventNearToProvidedEventToSelection( + tr.b.getFirstElement(sel), 1, selRight); + assert.isTrue(ret); + assert.strictEqual( + track.rects[2].modelItem, tr.b.getFirstElement(selRight)); + + // Select to the right of the 2nd b. + const selRight2 = new EventSet(); + ret = track.addEventNearToProvidedEventToSelection( + tr.b.getFirstElement(sel), 2, selRight2); + assert.isTrue(ret); + assert.strictEqual( + track.rects[3].modelItem, tr.b.getFirstElement(selRight2)); + + // Select to 2 to the right of the 2nd b. + const selRightOfRight = new EventSet(); + ret = track.addEventNearToProvidedEventToSelection( + tr.b.getFirstElement(selRight), 1, selRightOfRight); + assert.isTrue(ret); + assert.strictEqual(track.rects[3].modelItem, + tr.b.getFirstElement(selRightOfRight)); + + // Select to the right of the rightmost slice. + let selNone = new EventSet(); + ret = track.addEventNearToProvidedEventToSelection( + tr.b.getFirstElement(selRightOfRight), 1, selNone); + assert.isFalse(ret); + assert.strictEqual(0, selNone.length); + + // Select A and then select left. + sel = new EventSet(); + track.addAllEventsMatchingFilterToSelection( + new tr.c.TitleOrCategoryFilter('a'), sel); + + selNone = new EventSet(); + ret = track.addEventNearToProvidedEventToSelection( + tr.b.getFirstElement(sel), -1, selNone); + assert.isFalse(ret); + assert.strictEqual(0, selNone.length); + }); + + test('rectTrackAddClosestEventToSelection', function() { + const track = new RectTrack(new tr.ui.TimelineViewport()); + track.rects = [ + new ThreadSlice('', 'a', 0, 1, {}, 1), + new ThreadSlice('', 'b', 1, 2.1, {}, 4.8), + new ThreadSlice('', 'b', 1, 7, {}, 0.5), + new ThreadSlice('', 'c', 2, 7.6, {}, 0.4) + ]; + + // Before with not range. + let sel = new EventSet(); + track.addClosestEventToSelection(0, 0, 0, 0, sel); + assert.strictEqual(0, sel.length); + + // Before with negative range. + sel = new EventSet(); + track.addClosestEventToSelection(1.5, -10, 0, 0, sel); + assert.strictEqual(0, sel.length); + + // Before first slice. + sel = new EventSet(); + track.addClosestEventToSelection(0.5, 1, 0, 0, sel); + assert.isTrue(sel.equals(new EventSet(track.rects[0].modelItem))); + + // Within first slice closer to start. + sel = new EventSet(); + track.addClosestEventToSelection(1.3, 1, 0, 0, sel); + assert.isTrue(sel.equals(new EventSet(track.rects[0].modelItem))); + + // Between slices with good range. + sel = new EventSet(); + track.addClosestEventToSelection(2.08, 3, 0, 0, sel); + assert.isTrue(sel.equals(new EventSet(track.rects[1].modelItem))); + + // Between slices with bad range. + sel = new EventSet(); + track.addClosestEventToSelection(2.05, 0.03, 0, 0, sel); + assert.strictEqual(0, sel.length); + + // Within slice closer to end. + sel = new EventSet(); + track.addClosestEventToSelection(6, 100, 0, 0, sel); + assert.isTrue(sel.equals(new EventSet(track.rects[1].modelItem))); + + // Within slice with bad range. + sel = new EventSet(); + track.addClosestEventToSelection(1.8, 0.1, 0, 0, sel); + assert.strictEqual(0, sel.length); + + // After last slice with good range. + sel = new EventSet(); + track.addClosestEventToSelection(8.5, 1, 0, 0, sel); + assert.isTrue(sel.equals(new EventSet(track.rects[3].modelItem))); + + // After last slice with bad range. + sel = new EventSet(); + track.addClosestEventToSelection(10, 1, 0, 0, sel); + assert.strictEqual(0, sel.length); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html new file mode 100644 index 00000000000..1f764019cfc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/tracks/rect_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays an array of Sample objects. + * @constructor + * @extends {RectTrack} + */ + const SampleTrack = tr.ui.b.define( + 'sample-track', tr.ui.tracks.RectTrack); + + SampleTrack.prototype = { + + __proto__: tr.ui.tracks.RectTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.RectTrack.prototype.decorate.call(this, viewport); + }, + + get samples() { + return this.rects; + }, + + set samples(samples) { + this.rects = samples; + } + }; + + return { + SampleTrack, + }; +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html new file mode 100644 index 00000000000..0fb17df65f1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/profile_node.html"> +<link rel="import" href="/tracing/model/sample.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> +<link rel="import" href="/tracing/ui/tracks/sample_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const SampleTrack = tr.ui.tracks.SampleTrack; + const Sample = tr.model.Sample; + const ProfileNode = tr.model.ProfileNode; + + test('modelMapping', function() { + const track = new SampleTrack(new tr.ui.TimelineViewport()); + const node = new ProfileNode(1, { + functionName: 'a' + }, undefined); + const sample = new Sample(10, 'instructions_retired', node); + track.samples = [sample]; + const me0 = track.rects[0].modelItem; + assert.strictEqual(me0, sample); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html new file mode 100644 index 00000000000..36f09566b07 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html @@ -0,0 +1,167 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/multi_row_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays a SliceGroup. + * @constructor + * @extends {MultiRowTrack} + */ + const SliceGroupTrack = tr.ui.b.define( + 'slice-group-track', tr.ui.tracks.MultiRowTrack); + + SliceGroupTrack.prototype = { + + __proto__: tr.ui.tracks.MultiRowTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('slice-group-track'); + this.group_ = undefined; + // Set the collapse threshold so we don't collapse by default, but the + // user can explicitly collapse if they want it. + this.defaultToCollapsedWhenSubRowCountMoreThan = 100; + }, + + addSubTrack_(slices) { + const track = new tr.ui.tracks.SliceTrack(this.viewport); + track.slices = slices; + Polymer.dom(this).appendChild(track); + return track; + }, + + get group() { + return this.group_; + }, + + set group(group) { + this.group_ = group; + this.setItemsToGroup(this.group_.slices, this.group_); + }, + + get eventContainer() { + return this.group; + }, + + addContainersToTrackMap(containerToTrackMap) { + tr.ui.tracks.MultiRowTrack.prototype.addContainersToTrackMap.apply( + this, arguments); + containerToTrackMap.addContainer(this.group, this); + }, + + /** + * Breaks up the list of slices into N rows, each of which is a list of + * slices that are non overlapping. + */ + buildSubRows_(slices) { + const precisionUnit = this.group.model.intrinsicTimeUnit; + + // This function works by walking through slices by start time. + // + // The basic idea here is to insert each slice as deep into the subrow + // list as it can go such that every subSlice is fully contained by its + // parent slice. + // + // Visually, if we start with this: + // 0: [ a ] + // 1: [ b ] + // 2: [c][d] + // + // To place this slice: + // [e] + // We first check row 2's last item, [d]. [e] wont fit into [d] (they dont + // even intersect). So we go to row 1. That gives us [b], and [d] wont fit + // into that either. So, we go to row 0 and its last slice, [a]. That can + // completely contain [e], so that means we should add [e] as a subchild + // of [a]. That puts it on row 1, yielding: + // 0: [ a ] + // 1: [ b ][e] + // 2: [c][d] + // + // If we then get this slice: + // [f] + // We do the same deepest-to-shallowest walk of the subrows trying to fit + // it. This time, it doesn't fit in any open slice. So, we simply append + // it to row 0: + // 0: [ a ] [f] + // 1: [ b ][e] + // 2: [c][d] + if (!slices.length) return []; + + const ops = []; + for (let i = 0; i < slices.length; i++) { + if (slices[i].subSlices) { + slices[i].subSlices.splice(0, + slices[i].subSlices.length); + } + ops.push(i); + } + + ops.sort(function(ix, iy) { + const x = slices[ix]; + const y = slices[iy]; + if (x.start !== y.start) return x.start - y.start; + + // Elements get inserted into the slices array in order of when the + // slices start. Because slices must be properly nested, we break + // start-time ties by assuming that the elements appearing earlier in + // the slices array (and thus ending earlier) start earlier. + return ix - iy; + }); + + const subRows = [[]]; + this.badSlices_ = []; // TODO(simonjam): Connect this again. + + for (let i = 0; i < ops.length; i++) { + const op = ops[i]; + const slice = slices[op]; + + // Try to fit the slice into the existing subrows. + let inserted = false; + for (let j = subRows.length - 1; j >= 0; j--) { + if (subRows[j].length === 0) continue; + + const insertedSlice = subRows[j][subRows[j].length - 1]; + if (slice.start < insertedSlice.start) { + this.badSlices_.push(slice); + inserted = true; + } + if (insertedSlice.bounds(slice, precisionUnit)) { + // Insert it into subRow j + 1. + while (subRows.length <= j + 1) { + subRows.push([]); + } + subRows[j + 1].push(slice); + if (insertedSlice.subSlices) { + insertedSlice.subSlices.push(slice); + } + inserted = true; + break; + } + } + if (inserted) continue; + + // Append it to subRow[0] as a root. + subRows[0].push(slice); + } + + return subRows; + } + }; + + return { + SliceGroupTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html new file mode 100644 index 00000000000..a8b5842f945 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html @@ -0,0 +1,299 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/slice_group.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ProcessTrack = tr.ui.tracks.ProcessTrack; + const ThreadTrack = tr.ui.tracks.ThreadTrack; + const SliceGroup = tr.model.SliceGroup; + const SliceGroupTrack = tr.ui.tracks.SliceGroupTrack; + const newSliceEx = tr.c.TestUtils.newSliceEx; + + test('subRowBuilderBasic', function() { + const m = new tr.Model(); + const t1 = m.getOrCreateProcess(1).getOrCreateThread(2); + const group = t1.sliceGroup; + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 2})); + const sB = group.pushSlice(newSliceEx({title: 'a', start: 3, duration: 1})); + + const track = new SliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = group; + const subRows = track.subRows; + + assert.strictEqual(track.badSlices_.length, 0); + assert.strictEqual(subRows.length, 1); + assert.strictEqual(subRows[0].length, 2); + assert.deepEqual(subRows[0], [sA, sB]); + }); + + test('subRowBuilderBasic2', function() { + const m = new tr.Model(); + const t1 = m.getOrCreateProcess(1).getOrCreateThread(2); + const group = t1.sliceGroup; + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 4})); + const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1})); + + const track = new SliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = group; + const subRows = track.subRows; + + assert.strictEqual(track.badSlices_.length, 0); + assert.strictEqual(subRows.length, 2); + assert.strictEqual(subRows[0].length, 1); + assert.strictEqual(subRows[1].length, 1); + assert.deepEqual(subRows[0], [sA]); + assert.deepEqual(subRows[1], [sB]); + }); + + test('subRowBuilderNestedExactly', function() { + const m = new tr.Model(); + const t1 = m.getOrCreateProcess(1).getOrCreateThread(2); + const group = t1.sliceGroup; + + const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, duration: 4})); + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 4})); + + const track = new SliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = group; + const subRows = track.subRows; + + assert.strictEqual(track.badSlices_.length, 0); + assert.strictEqual(subRows.length, 2); + assert.strictEqual(subRows[0].length, 1); + assert.strictEqual(subRows[1].length, 1); + assert.deepEqual(subRows[0], [sB]); + assert.deepEqual(subRows[1], [sA]); + }); + + test('subRowBuilderInstantEvents', function() { + const m = new tr.Model(); + const t1 = m.getOrCreateProcess(1).getOrCreateThread(2); + const group = t1.sliceGroup; + + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0})); + const sB = group.pushSlice(newSliceEx({title: 'b', start: 2, duration: 0})); + + const track = new SliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = group; + const subRows = track.subRows; + + assert.strictEqual(track.badSlices_.length, 0); + assert.strictEqual(subRows.length, 1); + assert.strictEqual(subRows[0].length, 2); + assert.deepEqual(subRows[0], [sA, sB]); + }); + + test('subRowBuilderTwoInstantEvents', function() { + const m = new tr.Model(); + const t1 = m.getOrCreateProcess(1).getOrCreateThread(2); + const group = t1.sliceGroup; + + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0})); + const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, duration: 0})); + + const track = new SliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = group; + const subRows = track.subRows; + + assert.strictEqual(track.badSlices_.length, 0); + assert.strictEqual(subRows.length, 2); + assert.deepEqual(subRows[0], [sA]); + assert.deepEqual(subRows[1], [sB]); + }); + + test('subRowBuilderOutOfOrderAddition', function() { + const m = new tr.Model(); + const t1 = m.getOrCreateProcess(1).getOrCreateThread(2); + const group = t1.sliceGroup; + + // Pattern being tested: + // [ a ][ b ] + // Where insertion is done backward. + const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1})); + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 2})); + + const track = new SliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = group; + const subRows = track.subRows; + + assert.strictEqual(track.badSlices_.length, 0); + assert.strictEqual(subRows.length, 1); + assert.strictEqual(subRows[0].length, 2); + assert.deepEqual(subRows[0], [sA, sB]); + }); + + test('subRowBuilderOutOfOrderAddition2', function() { + const m = new tr.Model(); + const t1 = m.getOrCreateProcess(1).getOrCreateThread(2); + const group = t1.sliceGroup; + + // Pattern being tested: + // [ a ] + // [ b ] + // Where insertion is done backward. + const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1})); + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 5})); + + const track = new SliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = group; + const subRows = track.subRows; + + assert.strictEqual(track.badSlices_.length, 0); + assert.strictEqual(subRows.length, 2); + assert.strictEqual(subRows[0].length, 1); + assert.strictEqual(subRows[1].length, 1); + assert.deepEqual(subRows[0], [sA]); + assert.deepEqual(subRows[1], [sB]); + }); + + test('subRowBuilderOnNestedZeroLength', function() { + const m = new tr.Model(); + const t1 = m.getOrCreateProcess(1).getOrCreateThread(2); + const group = t1.sliceGroup; + + // Pattern being tested: + // [ a ] + // [ b1 ] []<- b2 where b2.duration = 0 and b2.end === a.end. + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3})); + const sB1 = group.pushSlice(newSliceEx( + {title: 'b1', start: 1, duration: 2})); + const sB2 = group.pushSlice(newSliceEx( + {title: 'b2', start: 4, duration: 0})); + + const track = new SliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = group; + const subRows = track.subRows; + + assert.strictEqual(track.badSlices_.length, 0); + assert.strictEqual(subRows.length, 2); + assert.deepEqual(subRows[0], [sA]); + assert.deepEqual(subRows[1], [sB1, sB2]); + }); + + test('subRowBuilderOnGroup1', function() { + const m = new tr.Model(); + const t1 = m.getOrCreateProcess(1).getOrCreateThread(2); + const group = t1.sliceGroup; + + // Pattern being tested: + // [ a ] [ c ] + // [ b ] + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3})); + const sB = group.pushSlice(newSliceEx( + {title: 'b', start: 1.5, duration: 1})); + const sC = group.pushSlice(newSliceEx({title: 'c', start: 5, duration: 0})); + + const track = new SliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = group; + const subRows = track.subRows; + + assert.strictEqual(track.badSlices_.length, 0); + assert.strictEqual(subRows.length, 2); + assert.deepEqual(subRows[0], [sA, sC]); + assert.deepEqual(subRows[1], [sB]); + }); + + test('subRowBuilderOnGroup2', function() { + const m = new tr.Model(); + const t1 = m.getOrCreateProcess(1).getOrCreateThread(2); + const group = t1.sliceGroup; + + // Pattern being tested: + // [ a ] [ d ] + // [ b ] + // [ c ] + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3})); + const sB = group.pushSlice(newSliceEx( + {title: 'b', start: 1.5, duration: 1})); + const sC = group.pushSlice(newSliceEx( + {title: 'c', start: 1.75, duration: 0.5})); + const sD = group.pushSlice(newSliceEx( + {title: 'c', start: 5, duration: 0.25})); + + const track = new SliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = group; + + const subRows = track.subRows; + assert.strictEqual(track.badSlices_.length, 0); + assert.strictEqual(subRows.length, 3); + assert.deepEqual(subRows[0], [sA, sD]); + assert.deepEqual(subRows[1], [sB]); + assert.deepEqual(subRows[2], [sC]); + }); + + test('trackFiltering', function() { + const m = new tr.Model(); + const t1 = m.getOrCreateProcess(1).getOrCreateThread(2); + const group = t1.sliceGroup; + + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3})); + const sB = group.pushSlice(newSliceEx( + {title: 'b', start: 1.5, duration: 1})); + + const track = new SliceGroupTrack(new tr.ui.TimelineViewport()); + track.group = group; + + assert.strictEqual(track.subRows.length, 2); + assert.isTrue(track.hasVisibleContent); + }); + + test('sliceGroupContainerMap', function() { + const vp = new tr.ui.TimelineViewport(); + const containerToTrack = vp.containerToTrackMap; + const model = new tr.Model(); + const process = model.getOrCreateProcess(123); + const thread = process.getOrCreateThread(456); + const group = new SliceGroup(thread); + + const processTrack = new ProcessTrack(vp); + const threadTrack = new ThreadTrack(vp); + const groupTrack = new SliceGroupTrack(vp); + processTrack.process = process; + threadTrack.thread = thread; + groupTrack.group = group; + Polymer.dom(processTrack).appendChild(threadTrack); + Polymer.dom(threadTrack).appendChild(groupTrack); + + assert.strictEqual(processTrack.eventContainer, process); + assert.strictEqual(threadTrack.eventContainer, thread); + assert.strictEqual(groupTrack.eventContainer, group); + + assert.isUndefined(containerToTrack.getTrackByStableId('123')); + assert.isUndefined(containerToTrack.getTrackByStableId('123.456')); + assert.isUndefined( + containerToTrack.getTrackByStableId('123.456.SliceGroup')); + + vp.modelTrackContainer = { + addContainersToTrackMap(containerToTrackMap) { + processTrack.addContainersToTrackMap(containerToTrackMap); + }, + addEventListener() {} + }; + vp.rebuildContainerToTrackMap(); + + // Check that all tracks call childs' addContainersToTrackMap() + // by checking the resulting map. + assert.strictEqual( + containerToTrack.getTrackByStableId('123'), processTrack); + assert.strictEqual( + containerToTrack.getTrackByStableId('123.456'), threadTrack); + assert.strictEqual( + containerToTrack.getTrackByStableId('123.456.SliceGroup'), groupTrack); + + // Check the track's eventContainer getter. + assert.strictEqual(processTrack.eventContainer, process); + assert.strictEqual(threadTrack.eventContainer, thread); + assert.strictEqual(groupTrack.eventContainer, group); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html new file mode 100644 index 00000000000..1e1386bff66 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/tracks/rect_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays an array of Slice objects. + * @constructor + * @extends {RectTrack} + */ + const SliceTrack = tr.ui.b.define( + 'slice-track', tr.ui.tracks.RectTrack); + + SliceTrack.prototype = { + + __proto__: tr.ui.tracks.RectTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.RectTrack.prototype.decorate.call(this, viewport); + }, + + get slices() { + return this.rects; + }, + + set slices(slices) { + this.rects = slices; + } + }; + + return { + SliceTrack, + }; +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html new file mode 100644 index 00000000000..7ba42d3dc79 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/slice.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> +<link rel="import" href="/tracing/ui/tracks/slice_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const SliceTrack = tr.ui.tracks.SliceTrack; + const ThreadSlice = tr.model.ThreadSlice; + + test('modelMapping', function() { + const track = new SliceTrack(new tr.ui.TimelineViewport()); + const slice = new ThreadSlice('', 'a', 0, 1, {}, 1); + track.slices = [slice]; + const me0 = track.rects[0].modelItem; + assert.strictEqual(slice, me0); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css new file mode 100644 index 00000000000..094eee0862d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css @@ -0,0 +1,7 @@ +/* Copyright (c) 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +.spacing-track { + height: 4px; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html new file mode 100644 index 00000000000..a321066daa2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="stylesheet" href="/tracing/ui/tracks/spacing_track.css"> + +<link rel="import" href="/tracing/ui/base/heading.html"> +<link rel="import" href="/tracing/ui/tracks/track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track used to provide whitespace between the tracks above and below it. + * + * @constructor + * @extends {tr.ui.tracks.Track} + */ + const SpacingTrack = tr.ui.b.define('spacing-track', tr.ui.tracks.Track); + + SpacingTrack.prototype = { + __proto__: tr.ui.tracks.Track.prototype, + + decorate(viewport) { + tr.ui.tracks.Track.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('spacing-track'); + + this.heading_ = document.createElement('tr-ui-b-heading'); + Polymer.dom(this).appendChild(this.heading_); + }, + + addAllEventsMatchingFilterToSelection(filter, selection) { + } + }; + + return { + SpacingTrack, + }; +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html new file mode 100644 index 00000000000..7a292c04113 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html @@ -0,0 +1,131 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/heading.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays traces as stacked bars. + * @constructor + * @extends {Track} + */ + const StackedBarsTrack = tr.ui.b.define( + 'stacked-bars-track', tr.ui.tracks.Track); + + StackedBarsTrack.prototype = { + + __proto__: tr.ui.tracks.Track.prototype, + + decorate(viewport) { + tr.ui.tracks.Track.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('stacked-bars-track'); + this.objectInstance_ = null; + + this.heading_ = document.createElement('tr-ui-b-heading'); + Polymer.dom(this).appendChild(this.heading_); + }, + + set heading(heading) { + this.heading_.heading = heading; + }, + + get heading() { + return this.heading_.heading; + }, + + set tooltip(tooltip) { + this.heading_.tooltip = tooltip; + }, + + addEventsToTrackMap(eventToTrackMap) { + const objectSnapshots = this.objectInstance_.snapshots; + objectSnapshots.forEach(function(obj) { + eventToTrackMap.addEvent(obj, this); + }, this); + }, + + /** + * Used to hit-test clicks in the graph. + */ + addIntersectingEventsInRangeToSelectionInWorldSpace( + loWX, hiWX, viewPixWidthWorld, selection) { + function onSnapshot(snapshot) { + selection.push(snapshot); + } + + const snapshots = this.objectInstance_.snapshots; + const maxBounds = this.objectInstance_.parent.model.bounds.max; + + tr.b.iterateOverIntersectingIntervals( + snapshots, + function(x) { return x.ts; }, + function(x, i) { + if (i === snapshots.length - 1) { + if (snapshots.length === 1) { + return maxBounds; + } + + return snapshots[i].ts - snapshots[i - 1].ts; + } + + return snapshots[i + 1].ts - snapshots[i].ts; + }, + loWX, hiWX, + onSnapshot); + }, + + /** + * Add the item to the left or right of the provided item, if any, to the + * selection. + * @param {slice} The current slice. + * @param {Number} offset Number of slices away from the object to look. + * @param {Selection} selection The selection to add an event to, + * if found. + * @return {boolean} Whether an event was found. + * @private + */ + addEventNearToProvidedEventToSelection(event, offset, selection) { + if (!(event instanceof tr.model.ObjectSnapshot)) { + throw new Error('Unrecognized event'); + } + const objectSnapshots = this.objectInstance_.snapshots; + const index = objectSnapshots.indexOf(event); + const newIndex = index + offset; + if (newIndex >= 0 && newIndex < objectSnapshots.length) { + selection.push(objectSnapshots[newIndex]); + return true; + } + return false; + }, + + addAllEventsMatchingFilterToSelection(filter, selection) { + }, + + addClosestEventToSelection(worldX, worldMaxDist, loY, hiY, + selection) { + const snapshot = tr.b.findClosestElementInSortedArray( + this.objectInstance_.snapshots, + function(x) { return x.ts; }, + worldX, + worldMaxDist); + + if (!snapshot) return; + + selection.push(snapshot); + } + }; + + return { + StackedBarsTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css new file mode 100644 index 00000000000..4e063bbad48 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css @@ -0,0 +1,10 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +.thread-track { + flex-direction: column; + display: flex; + position: relative; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html new file mode 100644 index 00000000000..c6ea8fa576c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html @@ -0,0 +1,185 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="stylesheet" href="/tracing/ui/tracks/thread_track.css"> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/filter.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/async_slice_group_track.html"> +<link rel="import" href="/tracing/ui/tracks/container_track.html"> +<link rel="import" href="/tracing/ui/tracks/sample_track.html"> +<link rel="import" href="/tracing/ui/tracks/slice_group_track.html"> +<link rel="import" href="/tracing/ui/tracks/slice_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * Visualizes a Thread using a series of SliceTracks. + * @constructor + */ + const ThreadTrack = tr.ui.b.define('thread-track', + tr.ui.tracks.ContainerTrack); + ThreadTrack.prototype = { + __proto__: tr.ui.tracks.ContainerTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('thread-track'); + this.heading_ = document.createElement('tr-ui-b-heading'); + }, + + get thread() { + return this.thread_; + }, + + set thread(thread) { + this.thread_ = thread; + this.updateContents_(); + }, + + get hasVisibleContent() { + return this.tracks_.length > 0; + }, + + get hasSlices() { + return this.thread_.asyncSliceGroup.length > 0 || + this.thread_.sliceGroup.length > 0; + }, + + get hasTimeSlices() { + return this.thread_.timeSlices; + }, + + get eventContainer() { + return this.thread; + }, + + addContainersToTrackMap(containerToTrackMap) { + tr.ui.tracks.ContainerTrack.prototype.addContainersToTrackMap.apply( + this, arguments); + containerToTrackMap.addContainer(this.thread, this); + }, + + updateContents_() { + this.detach(); + + if (!this.thread_) return; + + this.heading_.heading = this.thread_.userFriendlyName; + this.heading_.tooltip = this.thread_.userFriendlyDetails; + + if (this.thread_.asyncSliceGroup.length) { + this.appendAsyncSliceTracks_(); + } + + this.appendThreadSamplesTracks_(); + + let needsHeading = false; + if (this.thread_.timeSlices) { + const timeSlicesTrack = new tr.ui.tracks.SliceTrack(this.viewport); + timeSlicesTrack.heading = ''; + timeSlicesTrack.height = tr.ui.b.THIN_SLICE_HEIGHT + 'px'; + timeSlicesTrack.slices = this.thread_.timeSlices; + if (timeSlicesTrack.hasVisibleContent) { + needsHeading = true; + Polymer.dom(this).appendChild(timeSlicesTrack); + } + } + + if (this.thread_.sliceGroup.length) { + const track = new tr.ui.tracks.SliceGroupTrack(this.viewport); + track.heading = this.thread_.userFriendlyName; + track.tooltip = this.thread_.userFriendlyDetails; + track.group = this.thread_.sliceGroup; + if (track.hasVisibleContent) { + needsHeading = false; + Polymer.dom(this).appendChild(track); + } + } + + if (needsHeading) { + Polymer.dom(this).appendChild(this.heading_); + } + }, + + appendAsyncSliceTracks_() { + const subGroups = this.thread_.asyncSliceGroup.viewSubGroups; + // TODO(kraynov): Support nested sub-groups. + subGroups.forEach(function(subGroup) { + const asyncTrack = new tr.ui.tracks.AsyncSliceGroupTrack(this.viewport); + asyncTrack.group = subGroup; + asyncTrack.heading = subGroup.title; + if (asyncTrack.hasVisibleContent) { + Polymer.dom(this).appendChild(asyncTrack); + } + }, this); + }, + + appendThreadSamplesTracks_() { + const threadSamples = this.thread_.samples; + if (threadSamples === undefined || threadSamples.length === 0) { + return; + } + const samplesByTitle = {}; + threadSamples.forEach(function(sample) { + if (samplesByTitle[sample.title] === undefined) { + samplesByTitle[sample.title] = []; + } + samplesByTitle[sample.title].push(sample); + }); + + const sampleTitles = Object.keys(samplesByTitle); + sampleTitles.sort(); + + sampleTitles.forEach(function(sampleTitle) { + const samples = samplesByTitle[sampleTitle]; + const samplesTrack = new tr.ui.tracks.SampleTrack(this.viewport); + samplesTrack.group = this.thread_; + samplesTrack.samples = samples; + samplesTrack.heading = this.thread_.userFriendlyName + ': ' + + sampleTitle; + samplesTrack.tooltip = this.thread_.userFriendlyDetails; + samplesTrack.selectionGenerator = function() { + const selection = new tr.model.EventSet(); + for (let i = 0; i < samplesTrack.samples.length; i++) { + selection.push(samplesTrack.samples[i]); + } + return selection; + }; + Polymer.dom(this).appendChild(samplesTrack); + }, this); + }, + + collapsedDidChange(collapsed) { + if (collapsed) { + let h = parseInt(this.tracks[0].height); + for (let i = 0; i < this.tracks.length; ++i) { + if (h > 2) { + this.tracks[i].height = Math.floor(h) + 'px'; + } else { + this.tracks[i].style.display = 'none'; + } + h = h * 0.5; + } + } else { + for (let i = 0; i < this.tracks.length; ++i) { + this.tracks[i].height = this.tracks[0].height; + this.tracks[i].style.display = ''; + } + } + } + }; + + return { + ThreadTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html new file mode 100644 index 00000000000..1ece1aa3f93 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html @@ -0,0 +1,141 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/instant_event.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/timeline_track_view.html"> +<link rel="import" href="/tracing/ui/tracks/thread_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const HighlightInstantEvent = tr.model.ThreadHighlightInstantEvent; + const Process = tr.model.Process; + const EventSet = tr.model.EventSet; + const StackFrame = tr.model.StackFrame; + const Sample = tr.model.Sample; + const Thread = tr.model.Thread; + const ThreadSlice = tr.model.ThreadSlice; + const ThreadTrack = tr.ui.tracks.ThreadTrack; + const Viewport = tr.ui.TimelineViewport; + const newAsyncSlice = tr.c.TestUtils.newAsyncSlice; + const newAsyncSliceNamed = tr.c.TestUtils.newAsyncSliceNamed; + const newSliceEx = tr.c.TestUtils.newSliceEx; + + test('selectionHitTestingWithThreadTrack', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 4)); + t1.sliceGroup.pushSlice(new ThreadSlice('', 'b', 0, 5.1, {}, 4)); + + const testEl = document.createElement('div'); + Polymer.dom(testEl).appendChild( + tr.ui.b.createScopedStyle('heading { width: 100px; }')); + testEl.style.width = '600px'; + + const viewport = new Viewport(testEl); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(testEl).appendChild(drawingContainer); + + const track = new ThreadTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + drawingContainer.updateCanvasSizeIfNeeded_(); + track.thread = t1; + + const y = track.getBoundingClientRect().top; + const h = track.getBoundingClientRect().height; + const wW = 10; + const vW = drawingContainer.canvas.getBoundingClientRect().width; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, wW, vW); + track.viewport.setDisplayTransformImmediately(dt); + + let selection = new EventSet(); + const x = (1.5 / wW) * vW; + track.addIntersectingEventsInRangeToSelection( + x, x + 1, y, y + 1, selection); + assert.isTrue(selection.equals( + new EventSet([t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]]))); + + selection = new EventSet(); + track.addIntersectingEventsInRangeToSelection( + (1.5 / wW) * vW, (1.8 / wW) * vW, + y, y + h, selection); + assert.isTrue(selection.equals( + new EventSet([t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]]))); + }); + + test('filterThreadSlices', function() { + const model = new tr.Model(); + const thread = new Thread(new Process(model, 7), 1); + thread.sliceGroup.pushSlice(newSliceEx( + {title: 'a', start: 0, duration: 0})); + thread.asyncSliceGroup.push(newAsyncSliceNamed('a', 0, 5, thread, thread)); + const t = new ThreadTrack(new tr.ui.TimelineViewport()); + t.thread = thread; + + assert.strictEqual(t.tracks_.length, 2); + assert.instanceOf(t.tracks_[0], tr.ui.tracks.AsyncSliceGroupTrack); + assert.instanceOf(t.tracks_[1], tr.ui.tracks.SliceGroupTrack); + }); + + test('sampleThreadSlices', function() { + let thread; + let cpu; + const model = tr.c.TestUtils.newModelWithEvents([], { + shiftWorldToZero: false, + pruneContainers: false, + customizeModelCallback(model) { + cpu = model.kernel.getOrCreateCpu(1); + thread = model.getOrCreateProcess(1).getOrCreateThread(2); + + const nodeA = tr.c.TestUtils.newProfileNode(model, 'a'); + const nodeB = tr.c.TestUtils.newProfileNode(model, 'b', nodeA); + const nodeC = tr.c.TestUtils.newProfileNode(model, 'c', nodeB); + const nodeD = tr.c.TestUtils.newProfileNode(model, 'd', nodeA); + + model.samples.push(new Sample(10, 'instructions_retired', nodeC, thread, + undefined, 10)); + model.samples.push(new Sample(20, 'instructions_retired', nodeB, thread, + undefined, 10)); + model.samples.push(new Sample(30, 'instructions_retired', nodeB, thread, + undefined, 10)); + model.samples.push(new Sample(40, 'instructions_retired', nodeD, thread, + undefined, 10)); + + model.samples.push(new Sample(25, 'page_fault', nodeB, thread, + undefined, 10)); + model.samples.push(new Sample(35, 'page_fault', nodeD, thread, + undefined, 10)); + } + }); + + const t = new ThreadTrack(new tr.ui.TimelineViewport()); + t.thread = thread; + assert.strictEqual(t.tracks_.length, 2); + + // Instructions retired + const t0 = t.tracks_[0]; + assert.notEqual(t0.heading.indexOf('instructions_retired'), -1); + assert.instanceOf(t0, tr.ui.tracks.SampleTrack); + assert.strictEqual(t0.samples.length, 4); + t0.samples.forEach(function(s) { + assert.instanceOf(s, tr.model.Sample); + }); + + // page_fault + const t1 = t.tracks_[1]; + assert.notEqual(t1.heading.indexOf('page_fault'), -1); + assert.instanceOf(t1, tr.ui.tracks.SampleTrack); + assert.strictEqual(t1.samples.length, 2); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css new file mode 100644 index 00000000000..3d56eef5b8d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css @@ -0,0 +1,33 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +.track-button { + background-color: rgba(255, 255, 255, 0.5); + border: 1px solid rgba(0, 0, 0, 0.1); + color: rgba(0,0,0,0.2); + font-size: 10px; + height: 12px; + text-align: center; + width: 12px; +} + +.track-button:hover { + background-color: rgba(255, 255, 255, 1.0); + border: 1px solid rgba(0, 0, 0, 0.5); + box-shadow: 0 0 .05em rgba(0, 0, 0, 0.4); + color: rgba(0, 0, 0, 1); +} + +.track-close-button { + left: 2px; + position: absolute; + top: 2px; +} + +.track-collapse-button { + left: 3px; + position: absolute; + top: 2px; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html new file mode 100644 index 00000000000..fccad427740 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html @@ -0,0 +1,167 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="stylesheet" href="/tracing/ui/tracks/track.css"> + +<link rel="import" href="/tracing/ui/base/container_that_decorates_its_children.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * The base class for all tracks, which render data into a provided div. + * @constructor + */ + const Track = tr.ui.b.define('track', + tr.ui.b.ContainerThatDecoratesItsChildren); + Track.prototype = { + __proto__: tr.ui.b.ContainerThatDecoratesItsChildren.prototype, + + decorate(viewport) { + tr.ui.b.ContainerThatDecoratesItsChildren.prototype.decorate.call(this); + if (viewport === undefined) { + throw new Error('viewport is required when creating a Track.'); + } + + this.viewport_ = viewport; + Polymer.dom(this).classList.add('track'); + }, + + get viewport() { + return this.viewport_; + }, + + get drawingContainer() { + if (this instanceof tr.ui.tracks.DrawingContainer) return this; + let cur = this.parentElement; + while (cur) { + if (cur instanceof tr.ui.tracks.DrawingContainer) return cur; + cur = cur.parentElement; + } + return undefined; + }, + + get eventContainer() { + }, + + invalidateDrawingContainer() { + const dc = this.drawingContainer; + if (dc) dc.invalidate(); + }, + + context() { + // This is a little weird here, but we have to be able to walk up the + // parent tree to get the context. + if (!Polymer.dom(this).parentNode) return undefined; + + if (!Polymer.dom(this).parentNode.context) { + throw new Error('Parent container does not support context() method.'); + } + return Polymer.dom(this).parentNode.context(); + }, + + decorateChild_(childTrack) { + }, + + undecorateChild_(childTrack) { + if (childTrack.detach) { + childTrack.detach(); + } + }, + + updateContents_() { + }, + + /** + * Wrapper function around draw() that performs transformations on the + * context necessary for the track's contents to be drawn in the right place + * given the current pan and zoom. + */ + drawTrack(type) { + const ctx = this.context(); + + const pixelRatio = window.devicePixelRatio || 1; + const bounds = this.getBoundingClientRect(); + const canvasBounds = ctx.canvas.getBoundingClientRect(); + + ctx.save(); + ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top)); + + const dt = this.viewport.currentDisplayTransform; + const viewLWorld = dt.xViewToWorld(0); + const viewRWorld = dt.xViewToWorld(canvasBounds.width * pixelRatio); + const viewHeight = bounds.height * pixelRatio; + + this.draw(type, viewLWorld, viewRWorld, viewHeight); + ctx.restore(); + }, + + draw(type, viewLWorld, viewRWorld, viewHeight) { + }, + + addEventsToTrackMap(eventToTrackMap) { + }, + + addContainersToTrackMap(containerToTrackMap) { + }, + + addIntersectingEventsInRangeToSelection( + loVX, hiVX, loVY, hiVY, selection) { + const pixelRatio = window.devicePixelRatio || 1; + const dt = this.viewport.currentDisplayTransform; + const viewPixWidthWorld = dt.xViewVectorToWorld(1); + const loWX = dt.xViewToWorld(loVX * pixelRatio); + const hiWX = dt.xViewToWorld(hiVX * pixelRatio); + + const clientRect = this.getBoundingClientRect(); + const a = Math.max(loVY, clientRect.top); + const b = Math.min(hiVY, clientRect.bottom); + if (a > b) return; + + this.addIntersectingEventsInRangeToSelectionInWorldSpace( + loWX, hiWX, viewPixWidthWorld, selection); + }, + + addIntersectingEventsInRangeToSelectionInWorldSpace( + loWX, hiWX, viewPixWidthWorld, selection) { + }, + + /** + * Gets implemented by supporting track types. The method adds the event + * closest to worldX to the selection. + * + * @param {number} worldX The position that is looked for. + * @param {number} worldMaxDist The maximum distance allowed from worldX to + * the event. + * @param {number} loY Lower Y bound of the search interval in view space. + * @param {number} hiY Upper Y bound of the search interval in view space. + * @param {Selection} selection Selection to which to add hits. + */ + addClosestEventToSelection( + worldX, worldMaxDist, loY, hiY, selection) { + }, + + addClosestInstantEventToSelection(instantEvents, worldX, + worldMaxDist, selection) { + const instantEvent = tr.b.findClosestElementInSortedArray( + instantEvents, + function(x) { return x.start; }, + worldX, + worldMaxDist); + + if (!instantEvent) return; + + selection.push(instantEvent); + } + }; + + return { + Track, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html new file mode 100644 index 00000000000..620a35c8040 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html @@ -0,0 +1,309 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/draw_helpers.html"> +<link rel="import" href="/tracing/ui/base/heading.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/track.html"> + +<style> +.x-axis-track { + height: 12px; +} + +.x-axis-track.tall-mode { + height: 30px; +} +</style> + +<script> +'use strict'; + +tr.exportTo('tr.ui.tracks', function() { + /** + * A track that displays the x-axis. + * @constructor + * @extends {Track} + */ + const XAxisTrack = tr.ui.b.define('x-axis-track', tr.ui.tracks.Track); + + XAxisTrack.prototype = { + __proto__: tr.ui.tracks.Track.prototype, + + decorate(viewport) { + tr.ui.tracks.Track.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('x-axis-track'); + this.strings_secs_ = []; + this.strings_msecs_ = []; + this.strings_usecs_ = []; + this.strings_nsecs_ = []; + + this.viewportChange_ = this.viewportChange_.bind(this); + viewport.addEventListener('change', this.viewportChange_); + + const heading = document.createElement('tr-ui-b-heading'); + heading.arrowVisible = false; + Polymer.dom(this).appendChild(heading); + }, + + detach() { + tr.ui.tracks.Track.prototype.detach.call(this); + this.viewport.removeEventListener('change', + this.viewportChange_); + }, + + viewportChange_() { + if (this.viewport.interestRange.isEmpty) { + Polymer.dom(this).classList.remove('tall-mode'); + } else { + Polymer.dom(this).classList.add('tall-mode'); + } + }, + + draw(type, viewLWorld, viewRWorld, viewHeight) { + switch (type) { + case tr.ui.tracks.DrawType.GRID: + this.drawGrid_(viewLWorld, viewRWorld); + break; + case tr.ui.tracks.DrawType.MARKERS: + this.drawMarkers_(viewLWorld, viewRWorld); + break; + } + }, + + drawGrid_(viewLWorld, viewRWorld) { + const ctx = this.context(); + const pixelRatio = window.devicePixelRatio || 1; + + const canvasBounds = ctx.canvas.getBoundingClientRect(); + const trackBounds = this.getBoundingClientRect(); + const width = canvasBounds.width * pixelRatio; + const height = trackBounds.height * pixelRatio; + + const hasInterestRange = !this.viewport.interestRange.isEmpty; + + const xAxisHeightPx = hasInterestRange ? (height * 2) / 5 : height; + + const vp = this.viewport; + const dt = vp.currentDisplayTransform; + + vp.updateMajorMarkData(viewLWorld, viewRWorld); + const majorMarkDistanceWorld = vp.majorMarkWorldPositions.length > 1 ? + vp.majorMarkWorldPositions[1] - vp.majorMarkWorldPositions[0] : 0; + + const numTicksPerMajor = 5; + const minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor; + const minorMarkDistancePx = dt.xWorldVectorToView(minorMarkDistanceWorld); + + const minorTickHeight = Math.floor(xAxisHeightPx * 0.25); + + ctx.save(); + + ctx.lineWidth = Math.round(pixelRatio); + + // Apply subpixel translate to get crisp lines. + // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ + const crispLineCorrection = (ctx.lineWidth % 2) / 2; + ctx.translate(crispLineCorrection, -crispLineCorrection); + + ctx.fillStyle = 'rgb(0, 0, 0)'; + ctx.strokeStyle = 'rgb(0, 0, 0)'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + ctx.font = (9 * pixelRatio) + 'px sans-serif'; + + const tickLabels = []; + ctx.beginPath(); + for (let i = 0; i < vp.majorMarkWorldPositions.length; i++) { + const curXWorld = vp.majorMarkWorldPositions[i]; + const curXView = dt.xWorldToView(curXWorld); + const displayText = vp.majorMarkUnit.format( + curXWorld, {deltaValue: majorMarkDistanceWorld}); + ctx.fillText(displayText, curXView + (2 * pixelRatio), 0); + + // Draw major mark. + tr.ui.b.drawLine(ctx, curXView, 0, curXView, xAxisHeightPx); + + // Draw minor marks. + if (minorMarkDistancePx) { + for (let j = 1; j < numTicksPerMajor; ++j) { + const xView = Math.floor(curXView + minorMarkDistancePx * j); + tr.ui.b.drawLine(ctx, + xView, xAxisHeightPx - minorTickHeight, + xView, xAxisHeightPx); + } + } + } + + // Draw bottom bar. + ctx.strokeStyle = 'rgb(0, 0, 0)'; + tr.ui.b.drawLine(ctx, 0, height, width, height); + ctx.stroke(); + + // Give distance between directly adjacent markers. + if (!hasInterestRange) return; + + // Draw middle bar. + tr.ui.b.drawLine(ctx, 0, xAxisHeightPx, width, xAxisHeightPx); + ctx.stroke(); + + // Distance Variables. + let displayDistance; + const displayTextColor = 'rgb(0,0,0)'; + + // Arrow Variables. + const arrowSpacing = 10 * pixelRatio; + const arrowColor = 'rgb(128,121,121)'; + const arrowPosY = xAxisHeightPx * 1.75; + const arrowWidthView = 3 * pixelRatio; + const arrowLengthView = 10 * pixelRatio; + const spaceForArrowsView = 2 * (arrowWidthView + arrowSpacing); + + ctx.textBaseline = 'middle'; + ctx.font = (14 * pixelRatio) + 'px sans-serif'; + const textPosY = arrowPosY; + + const interestRange = vp.interestRange; + + // If the range is zero, draw it's min timestamp next to the line. + if (interestRange.range === 0) { + const markerWorld = interestRange.min; + const markerView = dt.xWorldToView(markerWorld); + + const textToDraw = vp.majorMarkUnit.format(markerWorld); + let textLeftView = markerView + 4 * pixelRatio; + const textWidthView = ctx.measureText(textToDraw).width; + + // Put text to the left in case it gets cut off. + if (textLeftView + textWidthView > width) { + textLeftView = markerView - 4 * pixelRatio - textWidthView; + } + + ctx.fillStyle = displayTextColor; + ctx.fillText(textToDraw, textLeftView, textPosY); + return; + } + + const leftMarker = interestRange.min; + const rightMarker = interestRange.max; + + const leftMarkerView = dt.xWorldToView(leftMarker); + const rightMarkerView = dt.xWorldToView(rightMarker); + + const distanceBetweenMarkers = interestRange.range; + const distanceBetweenMarkersView = + dt.xWorldVectorToView(distanceBetweenMarkers); + const positionInMiddleOfMarkersView = + leftMarkerView + (distanceBetweenMarkersView / 2); + + const textToDraw = vp.majorMarkUnit.format(distanceBetweenMarkers); + const textWidthView = ctx.measureText(textToDraw).width; + const spaceForArrowsAndTextView = + textWidthView + spaceForArrowsView + arrowSpacing; + + // Set text positions. + let textLeftView = positionInMiddleOfMarkersView - textWidthView / 2; + const textRightView = textLeftView + textWidthView; + + if (spaceForArrowsAndTextView > distanceBetweenMarkersView) { + // Print the display distance text right of the 2 markers. + textLeftView = rightMarkerView + 2 * arrowSpacing; + + // Put text to the left in case it gets cut off. + if (textLeftView + textWidthView > width) { + textLeftView = leftMarkerView - 2 * arrowSpacing - textWidthView; + } + + ctx.fillStyle = displayTextColor; + ctx.fillText(textToDraw, textLeftView, textPosY); + + // Draw the arrows pointing from outside in and a line in between. + ctx.strokeStyle = arrowColor; + ctx.beginPath(); + tr.ui.b.drawLine(ctx, leftMarkerView, arrowPosY, rightMarkerView, + arrowPosY); + ctx.stroke(); + + ctx.fillStyle = arrowColor; + tr.ui.b.drawArrow(ctx, + leftMarkerView - 1.5 * arrowSpacing, arrowPosY, + leftMarkerView, arrowPosY, + arrowLengthView, arrowWidthView); + tr.ui.b.drawArrow(ctx, + rightMarkerView + 1.5 * arrowSpacing, arrowPosY, + rightMarkerView, arrowPosY, + arrowLengthView, arrowWidthView); + } else if (spaceForArrowsView <= distanceBetweenMarkersView) { + let leftArrowStart; + let rightArrowStart; + if (spaceForArrowsAndTextView <= distanceBetweenMarkersView) { + // Print the display distance text. + ctx.fillStyle = displayTextColor; + ctx.fillText(textToDraw, textLeftView, textPosY); + + leftArrowStart = textLeftView - arrowSpacing; + rightArrowStart = textRightView + arrowSpacing; + } else { + leftArrowStart = positionInMiddleOfMarkersView; + rightArrowStart = positionInMiddleOfMarkersView; + } + + // Draw the arrows pointing inside out. + ctx.strokeStyle = arrowColor; + ctx.fillStyle = arrowColor; + tr.ui.b.drawArrow(ctx, + leftArrowStart, arrowPosY, + leftMarkerView, arrowPosY, + arrowLengthView, arrowWidthView); + tr.ui.b.drawArrow(ctx, + rightArrowStart, arrowPosY, + rightMarkerView, arrowPosY, + arrowLengthView, arrowWidthView); + } + + ctx.restore(); + }, + + drawMarkers_(viewLWorld, viewRWorld) { + const pixelRatio = window.devicePixelRatio || 1; + const trackBounds = this.getBoundingClientRect(); + const viewHeight = trackBounds.height * pixelRatio; + + if (!this.viewport.interestRange.isEmpty) { + this.viewport.interestRange.draw(this.context(), + viewLWorld, viewRWorld, viewHeight); + } + }, + + /** + * Adds items intersecting the given range to a selection. + * @param {number} loVX Lower X bound of the interval to search, in + * viewspace. + * @param {number} hiVX Upper X bound of the interval to search, in + * viewspace. + * @param {number} loVY Lower Y bound of the interval to search, in + * viewspace. + * @param {number} hiVY Upper Y bound of the interval to search, in + * viewspace. + * @param {Selection} selection Selection to which to add results. + */ + addIntersectingEventsInRangeToSelection( + loVX, hiVX, loY, hiY, selection) { + // Does nothing. There's nothing interesting to pick on the xAxis + // track. + }, + + addAllEventsMatchingFilterToSelection(filter, selection) { + } + }; + + return { + XAxisTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html new file mode 100644 index 00000000000..459c05cd122 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html @@ -0,0 +1,133 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> +<link rel="import" href="/tracing/ui/tracks/x_axis_track.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const div = document.createElement('div'); + + const viewport = new tr.ui.TimelineViewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = tr.ui.tracks.XAxisTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + this.addHTMLOutput(div); + + drawingContainer.invalidate(); + + const dt = new tr.ui.TimelineDisplayTransform(); + dt.setPanAndScale(0, track.clientWidth / 1000); + track.viewport.setDisplayTransformImmediately(dt); + }); + + test('instantiate_interestRange', function() { + const div = document.createElement('div'); + + const viewport = new tr.ui.TimelineViewport(div); + viewport.interestRange.min = 300; + viewport.interestRange.max = 300; + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = tr.ui.tracks.XAxisTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + this.addHTMLOutput(div); + + drawingContainer.invalidate(); + + const dt = new tr.ui.TimelineDisplayTransform(); + dt.setPanAndScale(0, track.clientWidth / 1000); + track.viewport.setDisplayTransformImmediately(dt); + }); + + test('instantiate_singlePointInterestRange', function() { + const div = document.createElement('div'); + + const viewport = new tr.ui.TimelineViewport(div); + viewport.interestRange.min = 300; + viewport.interestRange.max = 400; + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = tr.ui.tracks.XAxisTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + this.addHTMLOutput(div); + + drawingContainer.invalidate(); + + const dt = new tr.ui.TimelineDisplayTransform(); + dt.setPanAndScale(0, track.clientWidth / 1000); + track.viewport.setDisplayTransformImmediately(dt); + }); + + function testTimeMode(mode, testInstance, numDigits, opt_unit) { + const div = document.createElement('div'); + + const viewport = new tr.ui.TimelineViewport(div); + viewport.timeMode = mode; + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const trackContext = drawingContainer.ctx_; + const oldFillText = trackContext.fillText; + const fillTextText = []; + const fillTextThis = []; + trackContext.fillText = function(text, xPos, yPos) { + fillTextText.push(text); + fillTextThis.push(this); + return oldFillText.call(this, text, xPos, yPos); + }; + + const track = tr.ui.tracks.XAxisTrack(viewport); + Polymer.dom(drawingContainer).appendChild(track); + testInstance.addHTMLOutput(div); + + drawingContainer.invalidate(); + tr.b.forceAllPendingTasksToRunForTest(); + + const dt = new tr.ui.TimelineDisplayTransform(); + dt.setPanAndScale(0, track.clientWidth / 1000); + track.viewport.setDisplayTransformImmediately(dt); + + const formatter = + new Intl.NumberFormat(undefined, { numDigits, numDigits }); + const formatFunction = function(value) { + let valueString = value.toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: numDigits + }); + if (opt_unit) valueString += opt_unit; + return valueString; + }; + const expectedText = viewport.majorMarkWorldPositions.map( + formatFunction); + assert.strictEqual(fillTextText.length, fillTextThis.length); + for (let i = 0; i < fillTextText.length; i++) { + assert.deepEqual(fillTextText[i], expectedText[i]); + assert.strictEqual(fillTextThis[i], trackContext); + } + } + + test('instantiate_timeModeMs', function() { + testTimeMode(tr.ui.TimelineViewport.TimeMode.TIME_IN_MS, + this, 3, ' ms'); + }); + + test('instantiate_timeModeRevisions', function() { + testTimeMode(tr.ui.TimelineViewport.TimeMode.REVISIONS, this, 0); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state.html b/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state.html new file mode 100644 index 00000000000..75b616332c2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/brushing_state_controller.html"> + +<!-- +This element handles storing and retrieving the brushing state of arbitrary +views (e.g. analysis sub-views). An element can use it by instantiating it and +appending it to itself: + + <div id="some-view-with-specific-brushing-state"> + <tr-ui-b-view-specific-brushing-state view-id="unique-view-identifier"> + </tr-ui-b-view-specific-brushing-state> + ... other child elements ... + </div> + +The state can then be retrieved from and pushed to the state element as +follows: + + newStateElement.set(state); + state = newStateElement.get(); + +Under the hood, the state element searches the DOM tree for an ancestor element +with a brushingStateController field to persist the state (see the +tr.c.BrushingStateController and tr.ui.b.BrushingState classes for more +details). +--> +<dom-module id='tr-ui-b-view-specific-brushing-state'> + <template></template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-b-view-specific-brushing-state', + + /** Compulsory unique identifier of the associated view. */ + get viewId() { + return this.getAttribute('view-id'); + }, + + set viewId(viewId) { + Polymer.dom(this).setAttribute('view-id', viewId); + }, + + /** + * Retrieve the persisted state of the associated view. The returned object + * (or any of its fields) must not be modified by the caller (unless the + * object/field is treated as a reference). + * + * If no state has been persisted yet or there is no ancestor element with + * a brushingStateController field, this method returns undefined. + */ + get() { + const viewId = this.viewId; + if (!viewId) { + throw new Error('Element must have a view-id attribute!'); + } + + const brushingStateController = + tr.c.BrushingStateController.getControllerForElement(this); + if (!brushingStateController) return undefined; + + return brushingStateController.getViewSpecificBrushingState(viewId); + }, + + /** + * Persist the provided state of the associated view. The provided object + * (or any of its fields) must not be modified afterwards (unless the + * object/field is treated as a reference). + * + * If there is no ancestor element with a brushingStateController field, + * this method does nothing. + */ + set(state) { + const viewId = this.viewId; + if (!viewId) { + throw new Error('Element must have a view-id attribute!'); + } + + const brushingStateController = + tr.c.BrushingStateController.getControllerForElement(this); + if (!brushingStateController) return; + + brushingStateController.changeViewSpecificBrushingState(viewId, state); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state_test.html b/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state_test.html new file mode 100644 index 00000000000..ee920cb1100 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state_test.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/brushing_state_controller.html"> +<link rel="import" href="/tracing/ui/view_specific_brushing_state.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const BrushingStateController = tr.c.BrushingStateController; + + function setStateElement(containerEl, viewId) { + const stateElement = document.createElement( + 'tr-ui-b-view-specific-brushing-state'); + stateElement.viewId = viewId; + Polymer.dom(containerEl).appendChild(stateElement); + return stateElement; + } + + function addChildDiv(element) { + const child = element.ownerDocument.createElement('div'); + Polymer.dom(element).appendChild(child); + return child; + } + + function addShadowChildDiv(element) { + const shadowRoot = element.createShadowRoot(); + return addChildDiv(shadowRoot); + } + + test('instantiate_withoutBrushingStateController', function() { + const containerEl = document.createElement('div'); + + const stateElement1 = setStateElement(containerEl, 'test-view'); + assert.isUndefined(stateElement1.get()); + stateElement1.set({e: 2.71828}); + assert.isUndefined(stateElement1.get()); + }); + + test('instantiate_withBrushingStateController', function() { + const rootEl = document.createElement('div'); + const containerEl = addChildDiv(addShadowChildDiv(addChildDiv(rootEl))); + rootEl.brushingStateController = new BrushingStateController(undefined); + + const stateElement1 = setStateElement(containerEl, 'test-view'); + assert.isUndefined(stateElement1.get()); + stateElement1.set({e: 2.71828}); + assert.deepEqual(stateElement1.get(), {e: 2.71828}); + + const stateElement2 = setStateElement(containerEl, 'test-view-2'); + assert.isUndefined(stateElement2.get()); + stateElement2.set({pi: 3.14159}); + assert.deepEqual(stateElement2.get(), {pi: 3.14159}); + + const stateElement3 = setStateElement(containerEl, 'test-view'); + assert.deepEqual(stateElement3.get(), {e: 2.71828}); + + const stateElement4 = setStateElement(containerEl, 'test-view-2'); + assert.deepEqual(stateElement4.get(), {pi: 3.14159}); + }); +}); +</script> |