diff options
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/value/ui')
50 files changed, 11255 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span.html new file mode 100644 index 00000000000..228afbd2891 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span.html @@ -0,0 +1,350 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/fixed_color_scheme.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html"> +<link rel="import" href="/tracing/metrics/all_fixed_color_schemes.html"> +<link rel="import" href="/tracing/ui/base/column_chart.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html"> + +<dom-module id="tr-v-ui-breakdown-span"> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + #table_container { + display: flex; + flex: 0 0 auto; + } + #table { + max-height: 150px; + overflow-y: auto; + } + </style> + + <div id="empty">(empty)</div> + <div id="table_container"> + <div id="container"></div> + <span> + <tr-ui-b-table id="table"></tr-ui-b-table> + </span> + </div> + </template> +</dom-module> + +<script> +'use strict'; + +tr.exportTo('tr.v.ui', function() { + const DEFAULT_COLOR_SCHEME = new tr.b.SinebowColorGenerator(); + + function getHistogramName(histogram, diagnosticName, key) { + if (histogram === undefined) return undefined; + const nameMap = histogram.diagnostics.get(diagnosticName); + if (nameMap === undefined) return undefined; + return nameMap.get(key); + } + + class BreakdownTableSummaryRow { + constructor(displayElement, histogramNames) { + this.displayElement_ = displayElement; + this.histogramNames_ = histogramNames; + this.keySpan_ = undefined; + } + + get numberValue() { + // Prevent this row from appearing in the ColumnChart. + return undefined; + } + + get keySpan() { + if (this.keySpan_ === undefined) { + if (this.histogramNames_.length) { + this.keySpan_ = document.createElement('tr-ui-a-analysis-link'); + this.keySpan_.setSelectionAndContent( + this.histogramNames_, 'Select All'); + } else { + this.keySpan_ = 'Sum'; + } + } + return this.keySpan_; + } + + get name() { + return 'Sum'; + } + + get displayElement() { + return this.displayElement_; + } + + get stringPercent() { + return '100%'; + } + } + + class BreakdownTableRow { + constructor(name, value, histogramName, unit, color) { + this.name_ = name; + this.value_ = value; + this.histogramName_ = histogramName; + this.unit_ = unit; + + if (typeof value !== 'number') { + throw new Error('unsupported value ' + value); + } + + this.tableSum_ = undefined; + this.keySpan_ = undefined; + + this.color_ = color; + const hsl = this.color.toHSL(); + hsl.l *= 0.85; + this.highlightedColor_ = tr.b.Color.fromHSL(hsl); + + if (this.unit_) { + this.displayElement_ = tr.v.ui.createScalarSpan(this.numberValue, { + unit: this.unit_, + }); + } else { + this.displayElement_ = tr.ui.b.createSpan({ + textContent: this.stringValue, + }); + } + } + + get name() { + return this.name_; + } + + get color() { + return this.color_; + } + + get highlightedColor() { + return this.highlightedColor_; + } + + get keySpan() { + if (this.keySpan_ === undefined) { + if (this.histogramName_) { + this.keySpan_ = document.createElement('tr-ui-a-analysis-link'); + this.keySpan_.setSelectionAndContent( + [this.histogramName_], this.name); + this.keySpan_.color = this.color; + this.keySpan_.title = this.histogramName_; + } else { + this.keySpan_ = document.createElement('span'); + this.keySpan_.innerText = this.name; + this.keySpan_.style.color = this.color; + } + } + return this.keySpan_; + } + + /** + * @return {number|undefined} + */ + get numberValue() { + if (!isNaN(this.value_) && + (this.value_ !== Infinity) && + (this.value_ !== -Infinity) && + (this.value_ > 0)) return this.value_; + // Prevent this row from appearing in the ColumnChart. + return undefined; + } + + get stringValue() { + if ((this.unit_ !== undefined) && + !isNaN(this.value_) && + (this.value_ !== Infinity) && + (this.value_ !== -Infinity)) { + return this.unit_.format(this.value_); + } + return this.value_.toString(); + } + + set tableSum(s) { + this.tableSum_ = s; + } + + get stringPercent() { + if (this.tableSum_ === undefined) return ''; + const num = this.numberValue; + if (num === undefined) return ''; + return Math.floor(num * 100.0 / this.tableSum_) + '%'; + } + + get displayElement() { + return this.displayElement_; + } + + compare(other) { + if (this.numberValue === undefined) { + if (other.numberValue === undefined) { + return this.name.localeCompare(other.name); + } + return 1; + } + if (other.numberValue === undefined) { + return -1; + } + if (this.numberValue === other.numberValue) { + return this.name.localeCompare(other.name); + } + return other.numberValue - this.numberValue; + } + } + + Polymer({ + is: 'tr-v-ui-breakdown-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + created() { + this.chart_ = new tr.ui.b.ColumnChart(); + this.chart_.graphHeight = 130; + this.chart_.isStacked = true; + this.chart_.hideXAxis = true; + this.chart_.hideLegend = true; + this.chart_.enableHoverBox = false; + this.chart_.addEventListener('rect-mouseenter', + event => this.onRectMouseEnter_(event)); + this.chart_.addEventListener('rect-mouseleave', + event => this.onRectMouseLeave_(event)); + }, + + onRectMouseEnter_(event) { + for (const row of this.$.table.tableRows) { + if (row.name === event.rect.key) { + row.displayElement.style.background = event.rect.color; + row.keySpan.scrollIntoViewIfNeeded(); + } else { + row.displayElement.style.background = ''; + } + } + }, + + onRectMouseLeave_(event) { + for (const row of this.$.table.tableRows) { + row.displayElement.style.background = ''; + } + }, + + ready() { + Polymer.dom(this.$.container).appendChild(this.chart_); + + this.$.table.zebra = true; + this.$.table.showHeader = false; + this.$.table.tableColumns = [ + { + value: row => row.keySpan, + }, + { + value: row => row.displayElement, + align: tr.ui.b.TableFormat.ColumnAlignment.RIGHT, + }, + { + value: row => row.stringPercent, + align: tr.ui.b.TableFormat.ColumnAlignment.RIGHT, + }, + ]; + }, + + updateContents_() { + this.$.container.style.display = 'none'; + this.$.table.style.display = 'none'; + this.$.empty.style.display = 'block'; + + if (!this.diagnostic_) { + this.chart_.data = []; + return; + } + + if (this.histogram_) this.chart_.unit = this.histogram_.unit; + + let colorScheme = undefined; + // https://github.com/catapult-project/catapult/issues/2970 + if (this.diagnostic.colorScheme === + tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER) { + colorScheme = (name) => { + let cat = name.split(' '); + cat = cat[cat.length - 1]; + return tr.e.chrome.ChromeUserFriendlyCategoryDriver.getColor(cat); + }; + } else if (this.diagnostic.colorScheme !== undefined) { + colorScheme = (name) => tr.b.FixedColorSchemeRegistry.lookUp( + this.diagnostic.colorScheme).getColor(name); + } else { + colorScheme = (name) => DEFAULT_COLOR_SCHEME.colorForKey(name); + } + + const tableRows = []; + let tableSum = 0; + const histogramNames = []; + for (const [key, value] of this.diagnostic) { + const histogramName = getHistogramName( + this.histogram_, this.name_, key); + const row = new BreakdownTableRow( + key, value, histogramName, this.chart_.unit, colorScheme(key)); + tableRows.push(row); + if (row.numberValue !== undefined) tableSum += row.numberValue; + if (histogramName) { + histogramNames.push(histogramName); + } + } + tableRows.sort((x, y) => x.compare(y)); + + if (tableSum > 0) { + let summaryDisplayElement = tableSum; + if (this.chart_.unit !== undefined) { + summaryDisplayElement = this.chart_.unit.format(tableSum); + } + summaryDisplayElement = tr.ui.b.createSpan({ + textContent: summaryDisplayElement, + }); + tableRows.unshift(new BreakdownTableSummaryRow( + summaryDisplayElement, histogramNames)); + } + + const chartData = {x: 0}; + for (const row of tableRows) { + if (row.numberValue === undefined) continue; + + // Let the row compute its percentage. + row.tableSum = tableSum; + + // Add it to the chart. + chartData[row.name] = row.numberValue; + + // Configure the colors. + const dataSeries = this.chart_.getDataSeries(row.name); + dataSeries.color = row.color; + dataSeries.highlightedColor = row.highlightedColor; + } + + if (tableRows.length > 0) { + this.$.table.style.display = 'block'; + this.$.empty.style.display = 'none'; + this.$.table.tableRows = tableRows; + this.$.table.rebuild(); + } + + if (Object.keys(chartData).length > 1) { + this.$.container.style.display = 'block'; + this.$.empty.style.display = 'none'; + this.chart_.data = [chartData]; + } + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span_test.html new file mode 100644 index 00000000000..71079cdc856 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span_test.html @@ -0,0 +1,149 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/fixed_color_scheme.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/breakdown.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/ui/breakdown_span.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate_Breakdown', function() { + let breakdown = new tr.v.d.Breakdown(); + breakdown.colorScheme = + tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER; + breakdown.set('script', 42); + breakdown.set('style', 57); + + // Test weird numbers. + breakdown.set('ba---a', NaN); + breakdown.set('inf', Infinity); + breakdown.set('-inf', -Infinity); + breakdown.set('goose egg', 0); + breakdown.set('<0', -1); + + // Test lots of categories + for (let i = 0; i < 10; ++i) { + breakdown.set('cat ' + i, i); + } + + // Test round-tripping. + breakdown = tr.v.d.Diagnostic.fromDict(breakdown.asDict()); + + const span = tr.v.ui.createDiagnosticSpan(breakdown); + assert.strictEqual('TR-V-UI-BREAKDOWN-SPAN', span.tagName); + this.addHTMLOutput(span); + }); + + test('instantiate_BreakdownWithFixedColorScheme', function() { + const colorScheme = tr.b.FixedColorScheme.fromNames([ + 'foo', + 'bar', + ]); + tr.b.FixedColorSchemeRegistry.register(() => colorScheme, { + 'name': 'myColorScheme', + }); + + let breakdown = new tr.v.d.Breakdown(); + breakdown.colorScheme = 'myColorScheme'; + breakdown.set('foo', 42); + breakdown.set('bar', 57); + + // Test round-tripping. + breakdown = tr.v.d.Diagnostic.fromDict(breakdown.asDict()); + + const span = tr.v.ui.createDiagnosticSpan(breakdown); + span.updateContents_(); + assert.strictEqual( + span.chart_.getDataSeries('foo').color, colorScheme.getColor('foo')); + this.addHTMLOutput(span); + }); + + test('empty', function() { + const breakdown = new tr.v.d.Breakdown(); + const span = tr.v.ui.createDiagnosticSpan(breakdown); + assert.strictEqual('TR-V-UI-BREAKDOWN-SPAN', span.tagName); + this.addHTMLOutput(span); + }); + + test('emptyExceptForWeirdNumbers', function() { + const breakdown = new tr.v.d.Breakdown(); + breakdown.set('ba---a', NaN); + breakdown.set('inf', Infinity); + breakdown.set('-inf', -Infinity); + breakdown.set('goose egg', 0); + breakdown.set('<0', -1); + + const span = tr.v.ui.createDiagnosticSpan(breakdown); + assert.strictEqual('TR-V-UI-BREAKDOWN-SPAN', span.tagName); + this.addHTMLOutput(span); + }); + + test('correlate', function() { + const histograms = new tr.v.HistogramSet(); + const sample0Breakdown = new tr.v.d.Breakdown(); + sample0Breakdown.set('a', 5); + sample0Breakdown.set('b', 3); + sample0Breakdown.set('c', 2); + const sample1Breakdown = new tr.v.d.Breakdown(); + sample1Breakdown.set('a', 50); + sample1Breakdown.set('b', 30); + sample1Breakdown.set('c', 20); + const related = new tr.v.d.RelatedNameMap(); + related.set('a', histograms.createHistogram('root:a', + tr.b.Unit.byName.timeDurationInMs, [5, 50]).name); + related.set('b', tr.v.Histogram.create('root:b', + tr.b.Unit.byName.timeDurationInMs, [3, 30]).name); + related.set('c', tr.v.Histogram.create('root:c', + tr.b.Unit.byName.timeDurationInMs, [2, 20]).name); + const hist = histograms.createHistogram('root', + tr.b.Unit.byName.timeDurationInMs, [ + { + value: 10, + diagnostics: new Map([['breakdown', sample0Breakdown]]), + }, + { + value: 100, + diagnostics: new Map([['breakdown', sample1Breakdown]]), + }, + ], { + diagnostics: new Map([ + ['breakdown', related], + ]), + }); + const span = tr.v.ui.createDiagnosticSpan(sample0Breakdown, 'breakdown', + hist); + this.addHTMLOutput(span); + const links = tr.ui.b.findDeepElementsMatching(span, + 'tr-ui-a-analysis-link'); + assert.lengthOf(links, 4); + assert.strictEqual(links[0].title, ''); + assert.strictEqual(links[1].title, 'root:a'); + assert.strictEqual(links[2].title, 'root:b'); + assert.strictEqual(links[3].title, 'root:c'); + assert.strictEqual(links[0].textContent, 'Select All'); + assert.strictEqual(links[1].textContent, 'a'); + assert.strictEqual(links[2].textContent, 'b'); + assert.strictEqual(links[3].textContent, 'c'); + assert.lengthOf(links[0].selection, 3); + assert.strictEqual(links[0].selection[0], 'root:a'); + assert.strictEqual(links[0].selection[1], 'root:b'); + assert.strictEqual(links[0].selection[2], 'root:c'); + assert.lengthOf(links[1].selection, 1); + assert.strictEqual(links[1].selection[0], 'root:a'); + assert.lengthOf(links[2].selection, 1); + assert.strictEqual(links[2].selection[0], 'root:b'); + assert.lengthOf(links[3].selection, 1); + assert.strictEqual(links[3].selection[0], 'root:c'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span.html new file mode 100644 index 00000000000..08e0cc91dca --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html"> + +<dom-module id="tr-v-ui-collected-related-event-set-span"> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-collected-related-event-set-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + updateContents_() { + Polymer.dom(this).textContent = ''; + for (const [canonicalUrl, events] of this.diagnostic) { + const link = document.createElement('a'); + if (events.length === 1) { + const event = tr.b.getOnlyElement(events); + link.textContent = event.title + ' ' + + tr.b.Unit.byName.timeDurationInMs.format(event.duration); + } else { + link.textContent = events.length + ' events'; + } + link.href = canonicalUrl; + Polymer.dom(this).appendChild(link); + Polymer.dom(this).appendChild(document.createElement('br')); + } + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span_test.html new file mode 100644 index 00000000000..c14f75a9826 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span_test.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('merge', function() { + let aSlice; + let bSlice; + const model = tr.c.TestUtils.newModel(function(model) { + aSlice = tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + title: 'a', + start: 0, + duration: 10 + }); + bSlice = tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + title: 'b', + start: 1, + duration: 10 + }); + const thread = model.getOrCreateProcess(1).getOrCreateThread(2); + thread.sliceGroup.pushSlice(aSlice); + thread.sliceGroup.pushSlice(bSlice); + }); + assert.notEqual(aSlice.stableId, bSlice.stableId); + + const aHist = new tr.v.Histogram('a', tr.b.Unit.byName.count); + const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.count); + + aHist.diagnostics.set('events', new tr.v.d.RelatedEventSet(aSlice)); + bHist.diagnostics.set('events', new tr.v.d.RelatedEventSet(bSlice)); + + let mergedHist = aHist.clone(); + mergedHist.addHistogram(bHist); + mergedHist = tr.v.Histogram.fromDict(mergedHist.asDict()); + + const mergedEvents = mergedHist.diagnostics.get('events'); + const span = tr.v.ui.createDiagnosticSpan(mergedEvents); + assert.strictEqual( + 'TR-V-UI-COLLECTED-RELATED-EVENT-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span.html new file mode 100644 index 00000000000..29773057810 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html"> + +<dom-module id="tr-v-ui-date-range-span"> + <template> + <content></content> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-date-range-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + updateContents_() { + if (this.diagnostic === undefined) { + Polymer.dom(this).textContent = ''; + return; + } + + Polymer.dom(this).textContent = this.diagnostic.toString(); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span_test.html new file mode 100644 index 00000000000..3e7f02f0727 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span_test.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/diagnostics/date_range.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate_one', function() { + const diagnostic = new tr.v.d.DateRange(1496693745398); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-DATE-RANGE-SPAN', span.tagName); + this.addHTMLOutput(span); + }); + + test('instantiate_merged', function() { + const diagnostic = new tr.v.d.DateRange(1496693745398); + diagnostic.addDiagnostic(new tr.v.d.DateRange(1496693745399)); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-DATE-RANGE-SPAN', span.tagName); + this.addHTMLOutput(span); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table.html b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table.html new file mode 100644 index 00000000000..3a600c00925 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table.html @@ -0,0 +1,125 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<dom-module id="tr-v-ui-diagnostic-map-table"> + <template> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> + +<script> +'use strict'; + +tr.exportTo('tr.v.ui', function() { + function makeColumn(title, histogram) { + return { + title, + value(map) { + const diagnostic = map.get(title); + if (!diagnostic) return ''; + return tr.v.ui.createDiagnosticSpan(diagnostic, title, histogram); + } + }; + } + + Polymer({ + is: 'tr-v-ui-diagnostic-map-table', + + created() { + this.diagnosticMaps_ = undefined; + this.histogram_ = undefined; + this.isMetadata_ = false; + }, + + set histogram(h) { + this.histogram_ = h; + }, + + set isMetadata(m) { + this.isMetadata_ = m; + this.$.table.showHeader = !this.isMetadata_; + }, + + /** + * The |title| will be used as the heading for the column containing + * diagnostic-spans for |diagnosticMap|'s Diagnostics. + * + * @param {!Array.<!Object>} maps + * @param {!string} maps[].title + * @param {!tr.v.d.DiagnosticMap} maps[].diagnosticMap + */ + set diagnosticMaps(maps) { + this.diagnosticMaps_ = maps; + this.updateContents_(); + }, + + get diagnosticMaps() { + return this.diagnosticMaps_; + }, + + updateContents_() { + if (this.isMetadata_ && this.diagnosticMaps_.length !== 1) { + throw new Error( + 'Metadata diagnostic-map-tables require exactly 1 DiagnosticMap'); + } + if (this.diagnosticMaps_ === undefined || + this.diagnosticMaps_.length === 0) { + this.$.table.tableRows = []; + this.$.table.tableColumns = []; + return; + } + + let names = new Set(); + for (const map of this.diagnosticMaps_) { + for (const [name, diagnostic] of map) { + // https://github.com/catapult-project/catapult/issues/2842 + if (diagnostic instanceof tr.v.d.UnmergeableDiagnosticSet) continue; + if (diagnostic instanceof tr.v.d.CollectedRelatedEventSet) continue; + + names.add(name); + } + } + names = Array.from(names).sort(); + + const histogram = this.histogram_; + if (this.isMetadata_) { + const diagnosticMap = this.diagnosticMaps_[0]; + this.$.table.tableColumns = [ + { + value(name) { + return name.name; + } + }, + { + value(name) { + const diagnostic = diagnosticMap.get(name.name); + if (!diagnostic) return ''; + return tr.v.ui.createDiagnosticSpan( + diagnostic, name.name, histogram); + } + }, + ]; + this.$.table.tableRows = names.map(name => { + // tr-ui-b-table requires rows to be objects. + return {name}; + }); + } else { + this.$.table.tableColumns = names.map( + name => makeColumn(name, histogram)); + this.$.table.tableRows = this.diagnosticMaps_; + } + + this.$.table.rebuild(); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table_test.html new file mode 100644 index 00000000000..d5d4ac02761 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table_test.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_map_table.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const map0 = new tr.v.d.DiagnosticMap(); + map0.set('genericA', new tr.v.d.GenericSet([{a: 0}])); + map0.set('genericB', new tr.v.d.GenericSet([{b: 0}])); + const map1 = new tr.v.d.DiagnosticMap(); + map1.set('genericA', new tr.v.d.GenericSet([{a: 1}])); + map1.set('genericB', new tr.v.d.GenericSet([{b: 1}])); + const table = document.createElement('tr-v-ui-diagnostic-map-table'); + table.diagnosticMaps = [map0, map1]; + this.addHTMLOutput(table); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span.html new file mode 100644 index 00000000000..741fc07f58e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/diagnostic.html"> +<link rel="import" href="/tracing/value/ui/breakdown_span.html"> +<link rel="import" href="/tracing/value/ui/collected_related_event_set_span.html"> +<link rel="import" href="/tracing/value/ui/date_range_span.html"> +<link rel="import" href="/tracing/value/ui/generic_set_span.html"> +<link rel="import" href="/tracing/value/ui/related_event_set_span.html"> +<link rel="import" href="/tracing/value/ui/scalar_diagnostic_span.html"> +<link rel="import" href="/tracing/value/ui/unmergeable_diagnostic_set_span.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + /** + * Find the name of a polymer element registered to display |diagnostic| + * or one of its base classes. + * + * @param {!tr.v.d.Diagnostic} diagnostic + * @return {string} + */ + function findElementNameForDiagnostic(diagnostic) { + let typeInfo = undefined; + let curProto = diagnostic.constructor.prototype; + while (curProto) { + typeInfo = tr.v.d.Diagnostic.findTypeInfo(curProto.constructor); + if (typeInfo && typeInfo.metadata.elementName) break; + typeInfo = undefined; + curProto = curProto.__proto__; + } + + if (typeInfo === undefined) { + throw new Error( + diagnostic.constructor.name + + ' or a base class must have a registered elementName'); + } + + const tagName = typeInfo.metadata.elementName; + + if (tr.ui.b.isUnknownElementName(tagName)) { + throw new Error('Element not registered: ' + tagName); + } + + return tagName; + } + + /** + * Create a visualization for |diagnostic|. + * + * @param {!tr.v.d.Diagnostic} diagnostic + * @param {string} name + * @param {!tr.v.Histogram} histogram + * @return {Element} + */ + function createDiagnosticSpan(diagnostic, name, histogram) { + const tagName = findElementNameForDiagnostic(diagnostic); + const span = document.createElement(tagName); + if (span.build === undefined) throw new Error(tagName); + span.build(diagnostic, name, histogram); + return span; + } + + return { + createDiagnosticSpan, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span_behavior.html b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span_behavior.html new file mode 100644 index 00000000000..a40c15cb1c8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span_behavior.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + const DIAGNOSTIC_SPAN_BEHAVIOR = { + created() { + this.diagnostic_ = undefined; + this.name_ = undefined; + this.histogram_ = undefined; + }, + + attached() { + if (this.diagnostic_) this.updateContents_(); + }, + + get diagnostic() { + return this.diagnostic_; + }, + + build(diagnostic, name, histogram) { + this.diagnostic_ = diagnostic; + this.name_ = name; + this.histogram_ = histogram; + if (this.isAttached) this.updateContents_(); + }, + + updateContents_() { + throw new Error('dom-modules must override updateContents_()'); + } + }; + + return { + DIAGNOSTIC_SPAN_BEHAVIOR, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span.html new file mode 100644 index 00000000000..6f355e44478 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html"> + +<dom-module id="tr-v-ui-generic-set-span"> + <template> + <style> + a { + display: block; + } + </style> + + <tr-ui-a-generic-object-view id="generic"></tr-ui-a-generic-object-view> + + <div id="links"></div> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + function isLinkTuple(value) { + return ((value instanceof Array) && + (value.length === 2) && + (typeof value[0] === 'string') && + tr.b.isUrl(value[1])); + } + + Polymer({ + is: 'tr-v-ui-generic-set-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + updateContents_() { + this.$.generic.style.display = 'none'; + this.$.links.textContent = ''; + if (this.diagnostic === undefined) return; + const values = Array.from(this.diagnostic); + + let areAllStrings = true; + let areAllNumbers = true; + for (const value of values) { + if (typeof value !== 'number') { + areAllNumbers = false; + if (typeof value !== 'string' && !isLinkTuple(value)) { + areAllStrings = false; + break; + } + } + } + + if (!areAllStrings) { + this.$.generic.style.display = ''; + this.$.generic.object = values; + return; + } + + if (areAllNumbers) { + values.sort((x, y) => x - y); + } else { + values.sort(); + } + + for (const value of values) { + const link = {textContent: '' + value}; + if (isLinkTuple(value)) { + link.textContent = value[0]; + link.href = value[1]; + } else if (tr.b.isUrl(value)) { + link.href = value; + } + if (this.name_ === tr.v.d.RESERVED_NAMES.TRACE_URLS) { + link.textContent = value.substr(1 + value.lastIndexOf('/')); + } + const linkEl = tr.ui.b.createLink(link); + if (link.href) { + linkEl.target = '_blank'; + // In case there's a listener in the hierarchy that calls + // preventDefault(), stop the event from propagating to it so that + // clicking the link always opens it in a new tab. + linkEl.addEventListener('click', e => e.stopPropagation()); + } + this.$.links.appendChild(linkEl); + } + } + }); + + return { + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span_test.html new file mode 100644 index 00000000000..6bddec81aa6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span_test.html @@ -0,0 +1,112 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/generic_set.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('link_tuple', function() { + const diagnostic = new tr.v.d.GenericSet([ + ['label', 'http://example.com/'], + ]); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + const links = tr.ui.b.findDeepElementsMatching(span, 'a'); + assert.lengthOf(links, diagnostic.size); + assert.strictEqual('label', links[0].textContent); + assert.strictEqual('http://example.com/', links[0].href); + }); + + test('instantiate', function() { + const diagnostic = new tr.v.d.GenericSet([{foo: 'bar', baz: [42]}]); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + }); + + test('strings', function() { + const diagnostic = new tr.v.d.GenericSet([ + 'foo', 'bar', 1, 0, Infinity, NaN, + ]); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + const links = tr.ui.b.findDeepElementsMatching(span, 'a'); + assert.lengthOf(links, diagnostic.size); + assert.strictEqual(links[0].textContent, '0'); + assert.strictEqual(links[0].href, ''); + assert.strictEqual(links[1].textContent, '1'); + assert.strictEqual(links[1].href, ''); + assert.strictEqual(links[2].textContent, 'Infinity'); + assert.strictEqual(links[2].href, ''); + assert.strictEqual(links[3].textContent, 'NaN'); + assert.strictEqual(links[3].href, ''); + assert.strictEqual(links[4].textContent, 'bar'); + assert.strictEqual(links[4].href, ''); + assert.strictEqual(links[5].textContent, 'foo'); + assert.strictEqual(links[5].href, ''); + }); + + test('numbers', function() { + const diagnostic = new tr.v.d.GenericSet([10, 1, 0, 2, 11]); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + const links = tr.ui.b.findDeepElementsMatching(span, 'a'); + assert.lengthOf(links, diagnostic.size); + assert.strictEqual(links[0].textContent, '0'); + assert.strictEqual(links[0].href, ''); + assert.strictEqual(links[1].textContent, '1'); + assert.strictEqual(links[1].href, ''); + assert.strictEqual(links[2].textContent, '2'); + assert.strictEqual(links[2].href, ''); + assert.strictEqual(links[3].textContent, '10'); + assert.strictEqual(links[3].href, ''); + assert.strictEqual(links[4].textContent, '11'); + assert.strictEqual(links[4].href, ''); + }); + + test('urls', function() { + const urls = [ + 'http://google.com/', + 'http://cnn.com/', + ]; + const span = tr.v.ui.createDiagnosticSpan(new tr.v.d.GenericSet(urls)); + assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + const links = tr.ui.b.findDeepElementsMatching(span, 'a'); + assert.lengthOf(links, urls.length); + assert.strictEqual(links[0].textContent, urls[1]); + assert.strictEqual(links[0].href, urls[1]); + assert.strictEqual(links[1].textContent, urls[0]); + assert.strictEqual(links[1].href, urls[0]); + }); + + test('traceUrls', function() { + const urls = [ + 'https://console.developers.google.com/m/cloudstorage/b/chromium-telemetry/o/c.html', + 'file://d/e/f.html', + ]; + const span = tr.v.ui.createDiagnosticSpan( + new tr.v.d.GenericSet(urls), tr.v.d.RESERVED_NAMES.TRACE_URLS); + assert.strictEqual('TR-V-UI-GENERIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + const links = tr.ui.b.findDeepElementsMatching(span, 'a'); + assert.lengthOf(links, urls.length); + assert.strictEqual(links[0].textContent, 'f.html'); + assert.strictEqual(links[0].href, urls[1]); + assert.strictEqual(links[1].textContent, 'c.html'); + assert.strictEqual(links[1].href, urls[0]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram-set-view.md b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram-set-view.md new file mode 100644 index 00000000000..951e4a9918c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram-set-view.md @@ -0,0 +1,71 @@ +<!-- Copyright 2017 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> + +# HistogramSet UI Architecture + +Documentation for users of this UI is in [metrics-results-ui](/docs/metrics-results-ui.md). + +This document outlines the MVC architecture of the implementation of the UI. + * Model: [HistogramSetViewState](/tracing/tracing/value/ui/histogram_set_view_state.html) + * searchQuery: regex filters Histogram names + * referenceDisplayLabel selects the reference column in the table + * showAll: when false, only sourceHistograms are shown in the table + * groupings: array of HistogramGroupings configures how the hierarchy is constructed + * sortColumnIndex + * sortDescending + * constrainNameColumn: whether the Name column in the table is constrained to 300px + * tableRowStates: Map from row name to HistogramSetTableRowState + * subRows: Map from row name to HistogramSetTableRowState + * isExpanded: whether the row is expanded to show its subRows + * isOverviewed: whether the overview charts are displayed + * cells: map from column names to HistogramSetTableCellState: + * isOpen: whether the cell's histogram-span is open and displaying the BarChart and Diagnostics + * brushedBinRange: which bins are brushed in the BarChart + * mergeSampleDiagnostics: whether sample diagnostics are merged + * Setters delegate to the main entry point, update(delta), which dispatches an update event to listeners + * View-Controllers: + * [histogram-set-view](/tracing/tracing/value/ui/histogram_set_view.html): + * Main entry point: build(HistogramSet, progressIndicator):Promise + * Displays "zero Histograms" + * Listens for download-csv event from [histogram-set-controls](/tracing/tracing/value/ui/histogram_set_controls.html) + * gets leafHistograms from the [histogram-set-table](/tracing/tracing/value/ui/histogram_set_table.html) + * builds a CSV using [CSVBuilder](/tracing/tracing/value/csv_builder.html) + * Collects possible configurations of the HistogramSet and passes them to the child elements directly (not through the HistogramSetViewState!): + * Possible groupings + * displayLabels + * baseStatisticNames + * Contains child elements: + * [histogram-set-controls](/tracing/tracing/value/ui/histogram_set_controls.html) + * visualizes and controls the top half of HistogramSetViewState: + * searchQuery + * toggle display of all isOvervieweds + * referenceDisplayLabel + * showAll + * groupings + * Displays a button to download a CSV of the leafHistograms + * Displays a "Help" link to [metrics-results-ui](/docs/metrics-results-ui.md) + * [histogram-set-table](/tracing/tracing/value/ui/histogram_set_table.html) + * Visualizes and controls the bottom half of HistogramSetViewState: + * sortColumnIndex + * sortDescending + * constrainNameColumn + * HistogramSetTableRowStates + * Builds [HistogramSetTableRow](/tracing/tracing/value/ui/histogram_set_table_row.html)s containing + * [histogram-set-table-name-cell](/tracing/tracing/value/ui/histogram_set_table_name_cell.html) + * Toggles HistogramSetTableRowState.isOverviewed + * Overview [NameLineChart](/tracing/tracing/ui/base/name_line_chart.html) + * [histogram-set-table-cell](/tracing/tracing/value/ui/histogram_set_table_cell.html) + * (missing) / (empty) / (unmergeable) + * when closed, [scalar-span](/tracing/tracing/value/ui/scalar_span.html) displays a single summary statistic + * when open, [histogram-span](/tracing/tracing/value/ui/histogram_span.html) contains: + * [NameBarChart](/tracing/tracing/ui/base/name_bar_chart.html) visualizes and controls HistogramSetTableCellState.brushedBinRange + * [scalar-map-table](/tracing/tracing/value/ui/scalar_map_table.html) of statistics + * Two [diagnostic-map-tables](/tracing/tracing/value/ui/diagnostic_map_table.html): one for Histogram.diagnostics and another for the sample diagnostics + * A checkbox to visualize and control HistogramSetTableCellState.mergeSampleDiagnostics + * Overview [NameLineChart](/tracing/tracing/ui/base/name_line_chart.html) + * Main entry points: + * build(allHistograms, sourceHistograms, displayLabels, progressIndicator):Promise + * onViewStateUpdate_(delta) + * The [HistogramSetLocation](/tracing/tracing/value/ui/histogram_set_location.html) synchronizes the HistogramSetViewState with the URL using the HTML5 history API. diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_importer_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_importer_test.html new file mode 100644 index 00000000000..bb4e1e11ed0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_importer_test.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/histogram_importer.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(() => { + const kHtmlString = '<script>throw new Error("oops");<' + '/script>'; + + function createHistogram(id) { + const histogram = + new tr.v.Histogram('name<' + id + '>', tr.b.Unit.byName.count); + histogram.addSample(id); + histogram.customizeSummaryOptions({ + count: false, + max: false, + min: false, + std: false, + sum: false, + }); + histogram.diagnostics.set('html', new tr.v.d.GenericSet([kHtmlString])); + return histogram; + } + + test('importZeroHistograms', async function() { + const loadingEl = document.createElement('div'); + this.addHTMLOutput(loadingEl); + const importer = new tr.v.HistogramImporter(loadingEl); + const histogramData = '\n'; + + const view = document.createElement('tr-v-ui-histogram-set-view'); + view.style.display = 'none'; + this.addHTMLOutput(view); + + await importer.importHistograms(histogramData, view); + + assert.strictEqual('block', view.style.display); + assert.strictEqual(undefined, view.histograms); + }); + + test('importOneHistogram', async function() { + const loadingEl = document.createElement('div'); + this.addHTMLOutput(loadingEl); + const importer = new tr.v.HistogramImporter(loadingEl); + + const hist = createHistogram(42); + const histogramData = '\n' + JSON.stringify(hist.asDict()) + '\n'; + + const view = document.createElement('tr-v-ui-histogram-set-view'); + view.style.display = 'none'; + this.addHTMLOutput(view); + + await importer.importHistograms(histogramData, view); + + assert.strictEqual('none', loadingEl.style.display); + assert.strictEqual('block', view.style.display); + assert.lengthOf(view.histograms, 1); + const histogram = view.histograms.getHistogramNamed('name<42>'); + assert.strictEqual(kHtmlString, tr.b.getOnlyElement( + histogram.diagnostics.get('html'))); + assert.deepEqual([42], histogram.sampleValues); + }); + + test('importNHistogram', async function() { + const loadingEl = document.createElement('div'); + this.addHTMLOutput(loadingEl); + const importer = new tr.v.HistogramImporter(loadingEl); + + const kNofHistograms = 1000; + let histogramData = '\n'; + for (let i = 0; i < kNofHistograms; i++) { + const id = kNofHistograms * 100 + i; + const histogram = createHistogram(id); + histogramData += JSON.stringify(histogram.asDict()) + '\n'; + } + + const view = document.createElement('tr-v-ui-histogram-set-view'); + view.style.display = 'none'; + this.addHTMLOutput(view); + + await importer.importHistograms(histogramData, view); + + assert.strictEqual('none', loadingEl.style.display); + assert.strictEqual('block', view.style.display); + assert.lengthOf(view.histograms, kNofHistograms); + for (let i = 0; i < kNofHistograms; i++) { + const id = kNofHistograms * 100 + i; + const histogram = view.histograms.getHistogramNamed('name<' + id + '>'); + assert.strictEqual(kHtmlString, tr.b.getOnlyElement( + histogram.diagnostics.get('html'))); + assert.deepEqual([id], histogram.sampleValues); + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls.html new file mode 100644 index 00000000000..c55093a90ec --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls.html @@ -0,0 +1,557 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/timing.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/dropdown.html"> +<link rel="import" href="/tracing/ui/base/grouping_table_groupby_picker.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_controls_export.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html"> + +<dom-module id="tr-v-ui-histogram-set-controls"> + <template> + <style> + :host { + display: block; + } + + #help, #feedback { + display: none; + margin-left: 20px; + } + + #search_container { + display: inline-flex; + margin-right: 20px; + padding-bottom: 1px; + border-bottom: 1px solid darkgrey; + } + + #search { + border: 0; + max-width: 20em; + outline: none; + } + + #clear_search { + visibility: hidden; + height: 1em; + stroke: black; + stroke-width: 16; + } + + #controls { + white-space: nowrap; + } + + #show_overview, #hide_overview { + height: 1em; + margin-right: 20px; + } + + #show_overview { + stroke: blue; + stroke-width: 16; + } + + #show_overview:hover { + background: blue; + stroke: white; + } + + #hide_overview { + display: none; + stroke-width: 18; + stroke: black; + } + + #hide_overview:hover { + background: black; + stroke: white; + } + + #reference_display_label { + display: none; + margin-right: 20px; + } + + #alpha, #alpha_slider_container { + display: none; + } + + #alpha { + margin-right: 20px; + } + + #alpha_slider_container { + background: white; + border: 1px solid black; + flex-direction: column; + padding: 0.5em; + position: absolute; + z-index: 10; /* scalar-span uses z-index :-( */ + } + + #alpha_slider { + -webkit-appearance: slider-vertical; + align-self: center; + height: 200px; + width: 30px; + } + + #statistic { + display: none; + margin-right: 20px; + } + + #show_visualization { + margin-right: 20px; + } + + #export { + margin-right: 20px; + } + </style> + + <div id="controls"> + <span id="search_container"> + <input id="search" value="{{searchQuery::keyup}}" placeholder="Find Histogram name"> + <svg viewbox="0 0 128 128" id="clear_search" on-tap="clearSearch_"> + <g> + <title>Clear search</title> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </g> + </svg> + </span> + + <svg viewbox="0 0 128 128" id="show_overview" + on-tap="toggleOverviewLineCharts_"> + <g> + <title>Show overview charts</title> + <line x1="19" y1="109" x2="49" y2="49"/> + <line x1="49" y1="49" x2="79" y2="79"/> + <line x1="79" y1="79" x2="109" y2="19"/> + </g> + </svg> + <svg viewbox="0 0 128 128" id="hide_overview" + on-tap="toggleOverviewLineCharts_"> + <g> + <title>Hide overview charts</title> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </g> + </svg> + + <select id="reference_display_label" value="{{referenceDisplayLabel::change}}"> + <option value="">Select a reference column</option> + </select> + + <button id="alpha" on-tap="openAlphaSlider_">α=[[alphaString]]</button> + <div id="alpha_slider_container"> + <input type="range" id="alpha_slider" value="{{alphaIndex::change}}" min="0" max="18" on-blur="closeAlphaSlider_" on-input="updateAlpha_"> + </div> + + <select id="statistic" value="{{displayStatisticName::change}}"> + </select> + + <button id="show_visualization" on-tap="loadVisualization_">Visualize</button> + + <tr-ui-b-dropdown label="Export"> + <tr-v-ui-histogram-set-controls-export> + </tr-v-ui-histogram-set-controls-export> + </tr-ui-b-dropdown> + + <input type="checkbox" id="show_all" checked="{{showAll::change}}" title="When unchecked, less important histograms are hidden."> + <label for="show_all" title="When unchecked, less important histograms are hidden.">Show all</label> + + <a id="help">Help</a> + <a id="feedback">Feedback</a> + </div> + + <tr-ui-b-grouping-table-groupby-picker id="picker"> + </tr-ui-b-grouping-table-groupby-picker> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + const ALPHA_OPTIONS = []; + for (let i = 1; i < 10; ++i) ALPHA_OPTIONS.push(i * 1e-3); + for (let i = 1; i < 10; ++i) ALPHA_OPTIONS.push(i * 1e-2); + ALPHA_OPTIONS.push(0.1); + + Polymer({ + is: 'tr-v-ui-histogram-set-controls', + + properties: { + searchQuery: { + type: String, + value: '', + observer: 'onSearchQueryChange_', + }, + showAll: { + type: Boolean, + value: true, + observer: 'onUserChange_', + }, + referenceDisplayLabel: { + type: String, + value: '', + observer: 'onUserChange_', + }, + displayStatisticName: { + type: String, + value: '', + observer: 'onUserChange_', + }, + alphaString: { + type: String, + computed: 'getAlphaString_(alphaIndex)', + }, + alphaIndex: { + type: Number, + value: 9, + observer: 'onUserChange_', + }, + }, + + created() { + this.viewState_ = undefined; + this.rowListener_ = this.onRowViewStateUpdate_.bind(this); + this.baseStatisticNames_ = []; + + // When onViewStateUpdate_() copies multiple properties from the viewState + // to polymer properties, disable onUserChange_ until all properties are + // copied in order to prevent nested mutations to the ViewState. + this.isInOnViewStateUpdate_ = false; + this.searchQueryDebounceMs = 200; + }, + + ready() { + this.$.picker.addEventListener('current-groups-changed', + this.onGroupsChanged_.bind(this)); + }, + + get viewState() { + return this.viewState_; + }, + + set viewState(vs) { + if (this.viewState_) { + throw new Error('viewState must be set exactly once.'); + } + this.viewState_ = vs; + this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this)); + // It would be arduous to construct a delta and call viewStateListener_ + // here in case vs contains non-default values, so callers must set + // viewState first and then update it. + }, + + async onSearchQueryChange_() { + // Bypass debouncing for testing purpose: + if (this.searchQueryDebounceMs === 0) return this.onUserChange_(); + // Limit the update rate for instance caused by typing in a search. + this.debounce('onSearchQueryDebounce', this.onUserChange_, + this.searchQueryDebounceMs); + }, + + async onUserChange_() { + if (!this.viewState) return; + if (this.isInOnViewStateUpdate_) return; + + const marks = []; + if (this.searchQuery !== this.viewState.searchQuery) { + marks.push(tr.b.Timing.mark('histogram-set-controls', 'search')); + } + if (this.showAll !== this.viewState.showAll) { + marks.push(tr.b.Timing.mark('histogram-set-controls', 'showAll')); + } + if (this.referenceDisplayLabel !== this.viewState.referenceDisplayLabel) { + marks.push(tr.b.Timing.mark( + 'histogram-set-controls', 'referenceColumn')); + } + if (this.displayStatisticName !== this.viewState.displayStatisticName) { + marks.push(tr.b.Timing.mark('histogram-set-controls', 'statistic')); + } + if (parseInt(this.alphaIndex) !== this.getAlphaIndexFromViewState_()) { + marks.push(tr.b.Timing.mark('histogram-set-controls', 'alpha')); + } + + this.$.clear_search.style.visibility = + this.searchQuery ? 'visible' : 'hidden'; + + let displayStatisticName = this.displayStatisticName; + if (this.viewState.referenceDisplayLabel === '' && + this.referenceDisplayLabel !== '' && + this.baseStatisticNames.length) { + // The user selected a reference display label. + displayStatisticName = `%${tr.v.DELTA}${this.displayStatisticName}`; + // Can't set this.displayStatisticName before updating viewState -- that + // would cause an infinite loop of onUserChange_(). + } + if (this.referenceDisplayLabel === '' && + this.viewState.referenceDisplayLabel !== '' && + this.baseStatisticNames.length) { + // The user unset the reference display label. + // Ensure that displayStatisticName is not a delta statistic. + const deltaIndex = displayStatisticName.indexOf(tr.v.DELTA); + if (deltaIndex >= 0) { + displayStatisticName = displayStatisticName.slice(deltaIndex + 1); + } else if (!this.baseStatisticNames.includes(displayStatisticName)) { + displayStatisticName = 'avg'; + } + } + + // Propagate updates from the user to the view state. + await this.viewState.update({ + searchQuery: this.searchQuery, + showAll: this.showAll, + referenceDisplayLabel: this.referenceDisplayLabel, + displayStatisticName, + alpha: ALPHA_OPTIONS[this.alphaIndex], + }); + + if (this.referenceDisplayLabel && + this.statisticNames.length === this.baseStatisticNames.length) { + // When a reference column is selected, delta statistics should be + // available. + this.statisticNames = this.baseStatisticNames.concat( + tr.v.Histogram.getDeltaStatisticsNames(this.baseStatisticNames)); + } else if (!this.referenceDisplayLabel && + this.statisticNames.length > this.baseStatisticNames.length) { + // When a reference column is not selected, delta statistics should not + // be available. + this.statisticNames = this.baseStatisticNames; + } + + for (const mark of marks) mark.end(); + }, + + onViewStateUpdate_(event) { + this.isInOnViewStateUpdate_ = true; + + if (event.delta.searchQuery) { + this.searchQuery = this.viewState.searchQuery; + } + + if (event.delta.showAll) this.showAll = this.viewState.showAll; + + if (event.delta.displayStatisticName) { + this.displayStatisticName = this.viewState.displayStatisticName; + } + + if (event.delta.referenceDisplayLabel) { + this.referenceDisplayLabel = this.viewState.referenceDisplayLabel; + this.$.alpha.style.display = this.referenceDisplayLabel ? 'inline' : ''; + } + + if (event.delta.groupings) { + this.$.picker.currentGroupKeys = this.viewState.groupings.map( + g => g.key); + } + + if (event.delta.tableRowStates) { + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + this.viewState.tableRowStates.values())) { + row.addUpdateListener(this.rowListener_); + } + + const anyShowing = this.anyOverviewCharts_; + this.$.hide_overview.style.display = anyShowing ? 'inline' : 'none'; + this.$.show_overview.style.display = anyShowing ? 'none' : 'inline'; + } + + if (event.delta.alpha) { + this.alphaIndex = this.getAlphaIndexFromViewState_(); + } + + this.isInOnViewStateUpdate_ = false; + this.onUserChange_(); + }, + + onRowViewStateUpdate_(event) { + if (event.delta.isOverviewed) { + const anyShowing = event.delta.isOverviewed.current || + this.anyOverviewCharts_; + this.$.hide_overview.style.display = anyShowing ? 'inline' : 'none'; + this.$.show_overview.style.display = anyShowing ? 'none' : 'inline'; + } + + if (event.delta.subRows) { + for (const subRow of event.delta.subRows.previous) { + subRow.removeUpdateListener(this.rowListener_); + } + for (const subRow of event.delta.subRows.current) { + subRow.addUpdateListener(this.rowListener_); + } + } + }, + + onGroupsChanged_() { + if (this.$.picker.currentGroups.length === 0 && + this.$.picker.possibleGroups.length > 0) { + // If the current groupings are now empty but there are possible + // groupings, then force there to be at least one grouping. + // The histogram-set-table requires there to be at least one grouping. + this.$.picker.currentGroupKeys = [this.$.picker.possibleGroups[0].key]; + } + this.viewState.groupings = this.$.picker.currentGroups; + }, + + set showAllEnabled(enable) { + if (!enable) this.$.show_all.checked = true; + this.$.show_all.disabled = !enable; + }, + + set possibleGroupings(groupings) { + this.$.picker.possibleGroups = groupings; + this.$.picker.style.display = (groupings.length < 2) ? 'none' : 'block'; + this.onGroupsChanged_(); + }, + + set displayLabels(labels) { + this.$.reference_display_label.style.display = + (labels.length < 2) ? 'none' : 'inline'; + + while (this.$.reference_display_label.children.length > 1) { + this.$.reference_display_label.removeChild( + this.$.reference_display_label.lastChild); + } + + for (const displayLabel of labels) { + const option = document.createElement('option'); + option.textContent = displayLabel; + option.value = displayLabel; + this.$.reference_display_label.appendChild(option); + } + + if (labels.includes(this.viewState.referenceDisplayLabel)) { + this.referenceDisplayLabel = this.viewState.referenceDisplayLabel; + } else { + this.viewState.referenceDisplayLabel = ''; + } + }, + + get baseStatisticNames() { + return this.baseStatisticNames_; + }, + + set baseStatisticNames(names) { + this.baseStatisticNames_ = names; + this.statisticNames = names; + }, + + get statisticNames() { + return Array.from(this.$.statistic.options).map(o => o.value); + }, + + set statisticNames(names) { + this.$.statistic.style.display = (names.length < 2) ? 'none' : 'inline'; + + while (this.$.statistic.children.length) { + this.$.statistic.removeChild(this.$.statistic.lastChild); + } + + for (const name of names) { + const option = document.createElement('option'); + option.textContent = name; + this.$.statistic.appendChild(option); + } + + if (names.includes(this.viewState.displayStatisticName)) { + this.displayStatisticName = this.viewState.displayStatisticName; + // Polymer doesn't reset the value when the options change, so do that + // manually. + this.$.statistic.value = this.displayStatisticName; + } else { + this.viewState.displayStatisticName = names[0] || ''; + } + }, + + get anyOverviewCharts_() { + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + this.viewState.tableRowStates.values())) { + if (row.isOverviewed) return true; + } + return false; + }, + + async toggleOverviewLineCharts_() { + const showOverviews = !this.anyOverviewCharts_; + const mark = tr.b.Timing.mark('histogram-set-controls', + (showOverviews ? 'show' : 'hide') + 'OverviewCharts'); + + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + this.viewState.tableRowStates.values())) { + await row.update({isOverviewed: showOverviews}); + } + + this.$.hide_overview.style.display = showOverviews ? 'inline' : 'none'; + this.$.show_overview.style.display = showOverviews ? 'none' : 'inline'; + + await tr.b.animationFrame(); + mark.end(); + }, + + set helpHref(href) { + this.$.help.href = href; + this.$.help.style.display = 'inline'; + }, + + set feedbackHref(href) { + this.$.feedback.href = href; + this.$.feedback.style.display = 'inline'; + }, + + clearSearch_() { + this.set('searchQuery', ''); + this.$.search.focus(); + }, + + getAlphaString_(alphaIndex) { + // (9 * 1e-3).toString() is "0.009000000000000001", so truncate. + return ('' + ALPHA_OPTIONS[alphaIndex]).substr(0, 5); + }, + + openAlphaSlider_() { + const alphaButtonRect = this.$.alpha.getBoundingClientRect(); + this.$.alpha_slider_container.style.display = 'flex'; + this.$.alpha_slider_container.style.top = alphaButtonRect.bottom + 'px'; + this.$.alpha_slider_container.style.left = alphaButtonRect.left + 'px'; + this.$.alpha_slider.focus(); + }, + + closeAlphaSlider_() { + this.$.alpha_slider_container.style.display = ''; + }, + + updateAlpha_() { + this.alphaIndex = this.$.alpha_slider.value; + }, + + getAlphaIndexFromViewState_() { + for (let i = 0; i < ALPHA_OPTIONS.length; ++i) { + if (ALPHA_OPTIONS[i] >= this.viewState.alpha) return i; + } + return ALPHA_OPTIONS.length - 1; + }, + + set enableVisualization(enable) { + this.$.show_visualization.style.display = enable ? 'inline' : 'none'; + }, + + loadVisualization_() { + tr.b.dispatchSimpleEvent(this, 'loadVisualization', true, true, {}); + }, + }); + + return { + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html new file mode 100644 index 00000000000..98936f24bba --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/timing.html"> + +<dom-module id="tr-v-ui-histogram-set-controls-export"> + <template> + <style> + :host { + display: grid; + grid-gap: 1em; + grid-template-rows: auto auto; + grid-template-columns: auto auto; + } + button { + -webkit-appearance: none; + border: 0; + font-size: initial; + padding: 5px; + } + </style> + + <button on-tap="exportRawCsv_">raw CSV</button> + <button on-tap="exportRawJson_">raw JSON</button> + <button on-tap="exportMergedCsv_">merged CSV</button> + <button on-tap="exportMergedJson_">merged JSON</button> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-histogram-set-controls-export', + + exportRawCsv_() { + this.export_(false, 'csv'); + }, + + exportRawJson_() { + this.export_(false, 'json'); + }, + + exportMergedCsv_() { + this.export_(true, 'csv'); + }, + + exportMergedJson_() { + this.export_(true, 'json'); + }, + + export_(merged, format) { + tr.b.dispatchSimpleEvent(this, 'export', true, true, {merged, format}); + }, + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html new file mode 100644 index 00000000000..9783059ccbe --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html @@ -0,0 +1,300 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/histogram_grouping.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_controls.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(function() { + function buildControls(test) { + const controls = document.createElement('tr-v-ui-histogram-set-controls'); + controls.viewState = new tr.v.ui.HistogramSetViewState(); + test.addHTMLOutput(controls); + return controls; + } + + test('helpHref', function() { + const controls = buildControls(this); + controls.helpHref = 'data:text/html,hello'; + const help = tr.ui.b.findDeepElementMatchingPredicate( + controls, e => e.id === 'help'); + assert.strictEqual(help.style.display, 'inline'); + assert.strictEqual(help.href, 'data:text/html,hello'); + }); + + test('feedbackHref', function() { + const controls = buildControls(this); + controls.feedbackHref = 'data:text/html,hello'; + const feedback = tr.ui.b.findDeepElementMatchingPredicate( + controls, e => e.id === 'feedback'); + assert.strictEqual(feedback.style.display, 'inline'); + assert.strictEqual(feedback.href, 'data:text/html,hello'); + }); + + test('displayLabels', function() { + const controls = buildControls(this); + const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'reference_display_label'); + assert.strictEqual('none', getComputedStyle(selector).display); + + controls.displayLabels = []; + assert.strictEqual('none', getComputedStyle(selector).display); + + controls.displayLabels = ['Value']; + assert.strictEqual('none', getComputedStyle(selector).display); + + controls.displayLabels = ['a', 'b\nc']; + assert.strictEqual('inline-block', getComputedStyle(selector).display); + assert.strictEqual('', selector.children[0].value); + assert.strictEqual('a', selector.children[1].value); + assert.strictEqual('a', selector.children[1].textContent); + + // displayLabels can contain newlines, which <option> replace with spaces. + // histogram-set-controls must set option.value in order for selector.value + // to contain the newlines. + assert.strictEqual('b\nc', selector.children[2].value); + assert.strictEqual('b\nc', selector.children[2].textContent); + selector.selectedIndex = 2; + assert.strictEqual('b\nc', selector.value); + + controls.displayLabels = ['Value']; + assert.strictEqual('none', getComputedStyle(selector).display); + }); + + test('baseStatisticNames', function() { + const controls = buildControls(this); + controls.baseStatisticNames = ['avg', 'std']; + const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'statistic'); + assert.strictEqual('inline-block', getComputedStyle(selector).display); + assert.lengthOf(selector.children, 2); + assert.strictEqual('avg', selector.children[0].value); + assert.strictEqual('avg', selector.children[0].textContent); + assert.strictEqual('std', selector.children[1].value); + assert.strictEqual('std', selector.children[1].textContent); + assert.strictEqual('avg', selector.value); + }); + + test('viewDisplayStatisticName', function() { + const controls = buildControls(this); + controls.baseStatisticNames = ['avg', 'std']; + const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'statistic'); + controls.viewState.displayStatisticName = 'std'; + assert.strictEqual('std', selector.value); + }); + + test('controlDisplayStatisticName', function() { + const controls = buildControls(this); + controls.baseStatisticNames = ['avg', 'std']; + const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'statistic'); + selector.value = 'std'; + const changeEvent = document.createEvent('HTMLEvents'); + changeEvent.initEvent('change', false, true); + selector.dispatchEvent(changeEvent); + assert.strictEqual('std', controls.viewState.displayStatisticName); + }); + + test('viewSearchQuery', function() { + const controls = buildControls(this); + controls.viewState.searchQuery = 'foo'; + const search = tr.ui.b.findDeepElementMatchingPredicate( + controls, e => e.id === 'search'); + assert.strictEqual(search.value, 'foo'); + }); + + test('controlSearchQuery', function() { + const controls = buildControls(this); + controls.searchQueryDebounceMs = 0; + const search = tr.ui.b.findDeepElementMatching(controls, '#search'); + search.value = 'x'; + const keyupEvent = document.createEvent('KeyboardEvent'); + keyupEvent.initEvent('keyup'); + search.dispatchEvent(keyupEvent); + assert.strictEqual(controls.viewState.searchQuery, 'x'); + controls.clearSearch_(); + assert.strictEqual(controls.viewState.searchQuery, ''); + }); + + test('viewShowAll', function() { + const controls = buildControls(this); + const showAll = tr.ui.b.findDeepElementMatchingPredicate( + controls, e => e.id === 'show_all'); + assert.strictEqual(controls.viewState.showAll, true); + assert.strictEqual(showAll.checked, true); + controls.viewState.showAll = false; + assert.strictEqual(showAll.checked, false); + }); + + test('controlShowAll', function() { + const controls = buildControls(this); + const showAll = tr.ui.b.findDeepElementMatchingPredicate( + controls, e => e.id === 'show_all'); + assert.strictEqual(controls.viewState.showAll, true); + assert.strictEqual(showAll.checked, true); + showAll.click(); + assert.strictEqual(showAll.checked, false); + assert.strictEqual(controls.viewState.showAll, false); + const showAllLabel = tr.ui.b.findDeepElementMatchingPredicate( + controls, e => e.tagName === 'LABEL' && e.htmlFor === 'show_all'); + showAllLabel.click(); + assert.strictEqual(showAll.checked, true); + assert.strictEqual(controls.viewState.showAll, true); + }); + + test('viewReferenceDisplayLabel', function() { + const controls = buildControls(this); + controls.displayLabels = ['a', 'b']; + const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'reference_display_label'); + + assert.strictEqual('', selector.value); + assert.strictEqual('', controls.viewState.referenceDisplayLabel); + + controls.viewState.referenceDisplayLabel = 'a'; + assert.strictEqual('a', selector.value); + + controls.viewState.referenceDisplayLabel = 'b'; + assert.strictEqual('b', selector.value); + + controls.viewState.referenceDisplayLabel = ''; + assert.strictEqual('', selector.value); + }); + + test('controlReferenceDisplayLabel', function() { + const controls = buildControls(this); + controls.displayLabels = ['a', 'b']; + const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'reference_display_label'); + assert.strictEqual('', selector.value); + assert.strictEqual('', controls.viewState.referenceDisplayLabel); + + selector.value = 'a'; + const changeEvent = document.createEvent('HTMLEvents'); + changeEvent.initEvent('change', false, true); + selector.dispatchEvent(changeEvent); + assert.strictEqual('a', controls.viewState.referenceDisplayLabel); + + selector.value = 'b'; + selector.dispatchEvent(changeEvent); + assert.strictEqual('b', controls.viewState.referenceDisplayLabel); + + selector.value = ''; + selector.dispatchEvent(changeEvent); + assert.strictEqual('', controls.viewState.referenceDisplayLabel); + }); + + test('viewGroupings', function() { + const controls = buildControls(this); + const fooGrouping = new tr.v.HistogramGrouping('foo', h => 'foo'); + const groupings = Array.from(tr.v.HistogramGrouping.BY_KEY.values()); + groupings.push(fooGrouping); + controls.possibleGroupings = groupings; + const picker = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.tagName === 'TR-UI-B-GROUPING-TABLE-GROUPBY-PICKER'); + assert.lengthOf(picker.currentGroupKeys, 1); + assert.strictEqual(picker.currentGroupKeys[0], + tr.v.HistogramGrouping.HISTOGRAM_NAME.key); + + controls.viewState.groupings = [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + ]; + assert.lengthOf(picker.currentGroupKeys, 1); + assert.strictEqual(picker.currentGroupKeys[0], + tr.v.HistogramGrouping.HISTOGRAM_NAME.key); + assert.strictEqual('block', picker.style.display); + + controls.viewState.groupings = [ + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + fooGrouping, + ]; + assert.lengthOf(picker.currentGroupKeys, 2); + assert.strictEqual(picker.currentGroupKeys[0], + tr.v.d.RESERVED_NAMES.STORIES); + assert.strictEqual(picker.currentGroupKeys[1], 'foo'); + }); + + test('controlGroupings', function() { + const controls = buildControls(this); + const fooGrouping = new tr.v.HistogramGrouping('foo', h => 'foo'); + const groupings = Array.from(tr.v.HistogramGrouping.BY_KEY.values()); + groupings.push(fooGrouping); + controls.possibleGroupings = groupings; + const picker = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.tagName === 'TR-UI-B-GROUPING-TABLE-GROUPBY-PICKER'); + assert.lengthOf(picker.currentGroupKeys, 1); + assert.strictEqual(controls.viewState.groupings[0].key, + tr.v.HistogramGrouping.HISTOGRAM_NAME.key); + + picker.currentGroupKeys = ['name']; + assert.lengthOf(controls.viewState.groupings, 1); + assert.strictEqual(controls.viewState.groupings[0].key, + tr.v.HistogramGrouping.HISTOGRAM_NAME.key); + + picker.currentGroupKeys = [tr.v.d.RESERVED_NAMES.STORIES, 'foo']; + assert.lengthOf(controls.viewState.groupings, 2); + assert.strictEqual(controls.viewState.groupings[0], + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES)); + assert.strictEqual(controls.viewState.groupings[1], + fooGrouping); + }); + + test('viewIsOverviewed', function() { + const controls = buildControls(this); + const showOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'show_overview'); + const hideOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'hide_overview'); + controls.viewState.tableRowStates = new Map([ + ['a', new tr.v.ui.HistogramSetTableRowState()], + ['b', new tr.v.ui.HistogramSetTableRowState()], + ]); + assert.strictEqual('inline', showOverview.style.display); + assert.strictEqual('none', hideOverview.style.display); + + controls.viewState.tableRowStates.get('a').isOverviewed = true; + assert.strictEqual('none', showOverview.style.display); + assert.strictEqual('inline', hideOverview.style.display); + + controls.viewState.tableRowStates.get('a').isOverviewed = false; + assert.strictEqual('inline', showOverview.style.display); + assert.strictEqual('none', hideOverview.style.display); + }); + + test('controlIsOverviewed', async function() { + const controls = buildControls(this); + const showOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'show_overview'); + const hideOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e => + e.id === 'hide_overview'); + controls.viewState.tableRowStates = new Map([ + ['a', new tr.v.ui.HistogramSetTableRowState()], + ['b', new tr.v.ui.HistogramSetTableRowState()], + ]); + assert.isFalse(controls.viewState.tableRowStates.get('a').isOverviewed); + assert.isFalse(controls.viewState.tableRowStates.get('b').isOverviewed); + assert.strictEqual('inline', showOverview.style.display); + assert.strictEqual('none', hideOverview.style.display); + + await controls.toggleOverviewLineCharts_(); + assert.strictEqual('none', showOverview.style.display); + assert.strictEqual('inline', hideOverview.style.display); + assert.isTrue(controls.viewState.tableRowStates.get('a').isOverviewed); + assert.isTrue(controls.viewState.tableRowStates.get('b').isOverviewed); + + await controls.toggleOverviewLineCharts_(); + assert.strictEqual('inline', showOverview.style.display); + assert.strictEqual('none', hideOverview.style.display); + assert.isFalse(controls.viewState.tableRowStates.get('a').isOverviewed); + assert.isFalse(controls.viewState.tableRowStates.get('b').isOverviewed); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html new file mode 100644 index 00000000000..882806ba32b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html @@ -0,0 +1,251 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/math.html"> +<link rel="import" href="/tracing/base/timing.html"> +<link rel="import" href="/tracing/base/url_json.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + // This number is used to decide whether the tableStates can fit in the URL, + // and omit them if not. + // There is no specification for maximum URL length, so it is typically + // limited by hosts, not browsers. + // TODO(#3816) Tune this number. + const MAX_URL_LENGTH = 2048; + + // This class wraps |window.location| and |window.history| to allow tests to + // mock it. + class Locus { + get origin() { + return window.location.origin; + } + + get pathname() { + return window.location.pathname; + } + + get search() { + return window.location.search; + } + + get hash() { + return window.location.hash; + } + + get state() { + if (this.stateMode === '#') return this.hash.substr(1); + return this.search.substr(1); + } + + get stateMode() { + if (this.hash) return '#'; + return '?'; + } + + buildUrlFromState(state) { + let url = this.origin + this.pathname; + if (this.stateMode === '#') url += this.search; + url += this.stateMode + state; + return url; + } + + pushState(state) { + if (state === this.state) return; + + // TODO(#3837) When should this actually call pushState()? + window.history.replaceState(null, null, this.buildUrlFromState(state)); + } + + addPopStateListener(listener) { + window.addEventListener('popstate', listener); + } + } + + class HistogramSetLocation { + constructor(opt_location) { + // Optional dependency injection for testing. + this.location_ = opt_location || new Locus(); + this.location_.addPopStateListener(this.onPopState_.bind(this)); + + this.viewState_ = undefined; + this.rowListener_ = this.onRowStateUpdate_.bind(this); + this.cellListener_ = this.onCellStateUpdate_.bind(this); + + // pushState_ is disabled while handling onPopState_. + this.poppingState_ = false; + } + + /** + * @return {!tr.v.ui.HistogramSetViewState} + */ + get viewState() { + return this.viewState_; + } + + /** + * @param {!tr.v.ui.HistogramSetViewState} vs + */ + async build(vs) { + if (this.viewState !== undefined) { + throw new Error('viewState must be set exactly once.'); + } + this.viewState_ = vs; + this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this)); + + await this.onPopState_(); + } + + onViewStateUpdate_(event) { + if (event.delta.tableRowStates) { + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + event.delta.tableRowStates.previous.values())) { + row.removeUpdateListener(this.rowListener_); + for (const cell of row.cells.values()) { + cell.removeUpdateListener(this.cellListener_); + } + } + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + event.delta.tableRowStates.current.values())) { + row.addUpdateListener(this.rowListener_); + for (const cell of row.cells.values()) { + cell.addUpdateListener(this.cellListener_); + } + } + } + + this.pushState_(); + } + + onRowStateUpdate_(event) { + // This assumes that subRows and cells are not updated. + this.pushState_(); + } + + onCellStateUpdate_(event) { + this.pushState_(); + } + + pushState_() { + if (this.poppingState_) return; + const mark = tr.b.Timing.mark('HistogramSetLocation', 'pushState'); + + const params = new Map(); + if (this.viewState.searchQuery) { + params.set('q', this.viewState.searchQuery); + } + if (this.viewState.referenceDisplayLabel) { + params.set('r', this.viewState.referenceDisplayLabel); + } + params.set('s', this.viewState.displayStatisticName); + if (!this.viewState.showAll) params.set('m', ''); + params.set('g', this.viewState.groupings.map(g => g.key).join('.')); + if (this.viewState.sortColumnIndex !== undefined) { + params.set('c', '' + this.viewState.sortColumnIndex); + } + if (this.viewState.sortDescending) params.set('d', ''); + if (!this.viewState.constrainNameColumn) params.set('n', '0'); + if (!tr.b.math.approximately(this.viewState.alpha, 0.01)) { + params.set('p', ('' + this.viewState.alpha).substr(0, 5)); + } + + let urlState = ''; + for (const [key, value] of params) { + if (urlState) urlState += '&'; + urlState += key + '=' + window.encodeURIComponent(value); + } + + const rowDicts = {}; + for (const [name, rowState] of this.viewState.tableRowStates) { + const dict = rowState.asCompactDict(); + if (dict === undefined) continue; + rowDicts[name] = dict; + } + + if (Object.keys(rowDicts).length > 0) { + const rowsParam = '&t=' + tr.b.UrlJson.stringify(rowDicts); + + if (this.location_.buildUrlFromState(urlState + rowsParam).length < + MAX_URL_LENGTH) { + urlState += rowsParam; + } + } + + this.location_.pushState(urlState); + mark.end(); + } + + async onPopState_() { + const mark = tr.b.Timing.mark('HistogramSetLocation', 'onPopState'); + this.poppingState_ = true; + + const params = new Map(); + for (const kvp of this.location_.state.split('&')) { + const [key, value] = kvp.split('='); + try { + params.set(key, window.decodeURIComponent(value)); + } catch (e) { + // If the user tampers with the params so that a value cannot be + // decoded, ignore it. + } + } + + const delta = new Map(); + if (params.has('q')) delta.set('searchQuery', params.get('q')); + if (params.has('r')) delta.set('referenceDisplayLabel', params.get('r')); + if (params.has('s')) delta.set('displayStatisticName', params.get('s')); + delta.set('showAll', !params.has('m')); + if (params.has('g')) { + delta.set('groupings', params.get('g').split('.').map( + k => tr.v.HistogramGrouping.BY_KEY.get(k))); + } + if (params.has('c')) { + delta.set('sortColumnIndex', parseInt(params.get('c'))); + } else { + delta.set('sortColumnIndex', 0); + } + delta.set('sortDescending', params.has('d')); + delta.set('constrainNameColumn', params.get('n') !== '0'); + if (params.has('p')) { + delta.set('alpha', parseFloat(params.get('p'))); + } + + await this.viewState.update(delta); + + if (params.has('t')) { + let rowDicts; + try { + rowDicts = tr.b.UrlJson.parse(params.get('t')); + } catch (e) { + // If the user tampers with the params so that rowDicts cannot be + // parsed, ignore it. + } + + if (rowDicts) { + for (const [name, rowDict] of Object.entries(rowDicts)) { + const rowState = this.viewState.tableRowStates.get(name); + if (rowState === undefined) continue; + await rowState.updateFromCompactDict(rowDict); + } + } + } + + this.poppingState_ = false; + mark.end(); + } + } + + HistogramSetLocation.Locus = Locus; + + return { + HistogramSetLocation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html new file mode 100644 index 00000000000..d9987e7b097 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html @@ -0,0 +1,290 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/ui/histogram_set_location.html"> + +<script> +'use strict'; +/* eslint-disable max-len */ +tr.b.unittest.testSuite(function() { + class TestLocus extends tr.v.ui.HistogramSetLocation.Locus { + constructor() { + super(); + this.search_ = ''; + this.hash_ = ''; + this.listener_ = undefined; + } + + get origin() { + return 'http://example.com'; + } + + get pathname() { + return '/pathname'; + } + + get search() { + return this.search_; + } + + set search(s) { + this.search_ = s; + } + + get hash() { + return this.hash_; + } + + set hash(h) { + this.hash_ = h; + } + + pushState(state) { + if (this.hash) { + this.hash = '#' + state; + } else { + this.search = '?' + state; + } + } + + get stateMode() { + if (this.hash) return '#'; + return '?'; + } + + addPopStateListener(listener) { + this.listener_ = listener; + } + + async popState(state) { + if (state[0] === '?') { + this.search = state; + } else if (state[0] === '#') { + this.hash = state; + } + await this.listener_(); + } + } + + test('viewStateUpdateHashAndSearch', async function() { + const locus = new TestLocus(); + locus.search = '?'; + locus.hash = '#'; + const hsl = new tr.v.ui.HistogramSetLocation(locus); + await hsl.build(new tr.v.ui.HistogramSetViewState()); + + await hsl.viewState.update({ + displayStatisticName: 'avg', + groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ], + sortColumnIndex: undefined, + }); + assert.strictEqual(locus.hash, '#s=avg&g=name.stories'); + }); + + test('viewStateUpdateHash', async function() { + const locus = new TestLocus(); + locus.hash = '#'; + const hsl = new tr.v.ui.HistogramSetLocation(locus); + await hsl.build(new tr.v.ui.HistogramSetViewState()); + + await hsl.viewState.update({ + displayStatisticName: 'avg', + groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ], + sortColumnIndex: undefined, + }); + assert.strictEqual(locus.hash, '#s=avg&g=name.stories'); + + await hsl.viewState.update({searchQuery: 'foo'}); + assert.strictEqual(locus.hash, '#q=foo&s=avg&g=name.stories'); + + await hsl.viewState.update({referenceDisplayLabel: 'bar'}); + assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&g=name.stories'); + + await hsl.viewState.update({showAll: false}); + assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&m=&g=name.stories'); + + await hsl.viewState.update({sortColumnIndex: 2}); + assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2'); + + await hsl.viewState.update({sortDescending: true}); + assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d='); + + await hsl.viewState.update({constrainNameColumn: false}); + assert.strictEqual(locus.hash, + '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0'); + + const rowState = new tr.v.ui.HistogramSetTableRowState(); + rowState.cells.set('Value', new tr.v.ui.HistogramSetTableCellState()); + await hsl.viewState.update({tableRowStates: new Map([['fmp', rowState]])}); + assert.strictEqual(locus.hash, + '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0'); + + await hsl.viewState.tableRowStates.get('fmp').update({isExpanded: true}); + assert.strictEqual(locus.hash, + '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1)'); + + await hsl.viewState.tableRowStates.get('fmp').cells.get('Value').update({ + isOpen: true, + }); + assert.strictEqual(locus.hash, + '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1.c-(Value-(o-1)))'); + }); + + test('viewStateUpdateSearch', async function() { + const locus = new TestLocus(); + const hsl = new tr.v.ui.HistogramSetLocation(locus); + await hsl.build(new tr.v.ui.HistogramSetViewState()); + + await hsl.viewState.update({ + displayStatisticName: 'avg', + groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ], + sortColumnIndex: undefined, + }); + assert.strictEqual(locus.search, '?s=avg&g=name.stories'); + + await hsl.viewState.update({searchQuery: 'foo'}); + assert.strictEqual(locus.search, '?q=foo&s=avg&g=name.stories'); + + await hsl.viewState.update({referenceDisplayLabel: 'bar'}); + assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&g=name.stories'); + + await hsl.viewState.update({showAll: false}); + assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&m=&g=name.stories'); + + await hsl.viewState.update({sortColumnIndex: 2}); + assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2'); + + await hsl.viewState.update({sortDescending: true}); + assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d='); + + await hsl.viewState.update({constrainNameColumn: false}); + assert.strictEqual(locus.search, + '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0'); + + const rowState = new tr.v.ui.HistogramSetTableRowState(); + rowState.cells.set('Value', new tr.v.ui.HistogramSetTableCellState()); + await hsl.viewState.update({tableRowStates: new Map([['fmp', rowState]])}); + assert.strictEqual(locus.search, + '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0'); + + await hsl.viewState.tableRowStates.get('fmp').update({isExpanded: true}); + assert.strictEqual(locus.search, + '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1)'); + + await hsl.viewState.tableRowStates.get('fmp').cells.get('Value').update({ + isOpen: true, + }); + assert.strictEqual(locus.search, + '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1.c-(Value-(o-1)))'); + }); + + test('popStateSearch', async function() { + const locus = new TestLocus(); + const hsl = new tr.v.ui.HistogramSetLocation(locus); + await hsl.build(new tr.v.ui.HistogramSetViewState()); + + await locus.popState('?q=foo&r=bar&s=qux&m=&c=2&d=&g=name.stories'); + assert.strictEqual('foo', hsl.viewState.searchQuery); + assert.strictEqual('bar', hsl.viewState.referenceDisplayLabel); + assert.strictEqual('qux', hsl.viewState.displayStatisticName); + assert.isFalse(hsl.viewState.showAll); + assert.lengthOf(hsl.viewState.groupings, 2); + assert.strictEqual('name', hsl.viewState.groupings[0].key); + assert.strictEqual('stories', hsl.viewState.groupings[1].key); + assert.strictEqual(2, hsl.viewState.sortColumnIndex); + assert.isTrue(hsl.viewState.sortDescending); + + // onPopState_ should ignore missing rows and cells + await locus.popState('?t=f%3Am_p-(o-1)'); + assert.strictEqual(0, hsl.viewState.tableRowStates.size); + + await hsl.viewState.update({tableRowStates: new Map([ + ['f:m_p', new tr.v.ui.HistogramSetTableRowState()], + ])}); + assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').isExpanded); + + await locus.popState('?t=f%3Am_p-(e-1)'); + assert.strictEqual(0, hsl.viewState.tableRowStates.get('f:m_p').cells.size); + assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').isExpanded); + + await hsl.viewState.tableRowStates.get('f:m_p').update({cells: new Map([ + ['Value', new tr.v.ui.HistogramSetTableCellState()], + ])}); + assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen); + + await locus.popState('?t=f%3Am_p-(c-(Value-(o-1)))'); + assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen); + }); + + test('popStateHashAndSearch', async function() { + const locus = new TestLocus(); + const hsl = new tr.v.ui.HistogramSetLocation(locus); + await hsl.build(new tr.v.ui.HistogramSetViewState()); + + await locus.popState('?q=foo&r=bar&s=qux&a=&c=2&d=&g=name.stories'); + assert.strictEqual('foo', hsl.viewState.searchQuery); + assert.strictEqual('bar', hsl.viewState.referenceDisplayLabel); + assert.strictEqual('qux', hsl.viewState.displayStatisticName); + assert.isTrue(hsl.viewState.showAll); + assert.lengthOf(hsl.viewState.groupings, 2); + assert.strictEqual('name', hsl.viewState.groupings[0].key); + assert.strictEqual('stories', hsl.viewState.groupings[1].key); + assert.strictEqual(2, hsl.viewState.sortColumnIndex); + assert.isTrue(hsl.viewState.sortDescending); + + await locus.popState('#q=q'); + assert.strictEqual('q', hsl.viewState.searchQuery); + }); + + test('popStateHash', async function() { + const locus = new TestLocus(); + const hsl = new tr.v.ui.HistogramSetLocation(locus); + await hsl.build(new tr.v.ui.HistogramSetViewState()); + + await locus.popState('#q=foo&r=bar&s=qux&a=&c=2&d=&g=name.stories'); + assert.strictEqual('foo', hsl.viewState.searchQuery); + assert.strictEqual('bar', hsl.viewState.referenceDisplayLabel); + assert.strictEqual('qux', hsl.viewState.displayStatisticName); + assert.isTrue(hsl.viewState.showAll); + assert.lengthOf(hsl.viewState.groupings, 2); + assert.strictEqual('name', hsl.viewState.groupings[0].key); + assert.strictEqual('stories', hsl.viewState.groupings[1].key); + assert.strictEqual(2, hsl.viewState.sortColumnIndex); + assert.isTrue(hsl.viewState.sortDescending); + + // onPopState_ should ignore missing rows and cells + await locus.popState('#t=f%3Am_p-(o-1)'); + assert.strictEqual(0, hsl.viewState.tableRowStates.size); + + await hsl.viewState.update({tableRowStates: new Map([ + ['f:m_p', new tr.v.ui.HistogramSetTableRowState()], + ])}); + assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').isExpanded); + + await locus.popState('#t=f%3Am_p-(e-1)'); + assert.strictEqual(0, hsl.viewState.tableRowStates.get('f:m_p').cells.size); + assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').isExpanded); + + await hsl.viewState.tableRowStates.get('f:m_p').update({cells: new Map([ + ['Value', new tr.v.ui.HistogramSetTableCellState()], + ])}); + assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen); + + await locus.popState('#t=f%3Am_p-(c-(Value-(o-1)))'); + assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html new file mode 100644 index 00000000000..9ac3046c5d3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html @@ -0,0 +1,459 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/timing.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/histogram_set_hierarchy.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_table_row.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html"> + +<dom-module id="tr-v-ui-histogram-set-table"> + <template> + <style> + :host { + min-height: 0px; + overflow: auto; + } + #table { + margin-top: 5px; + } + </style> + + <tr-ui-b-table id="table"/> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + const MIDLINE_HORIZONTAL_ELLIPSIS = String.fromCharCode(0x22ef); + + // http://stackoverflow.com/questions/3446170 + function escapeRegExp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + } + + Polymer({ + is: 'tr-v-ui-histogram-set-table', + + created() { + this.viewState_ = undefined; + this.progress_ = () => Promise.resolve(); + this.nameColumnTitle_ = undefined; + this.displayLabels_ = []; + this.histograms_ = undefined; + this.sourceHistograms_ = undefined; + this.filteredHistograms_ = undefined; + this.groupedHistograms_ = undefined; + this.hierarchies_ = undefined; + this.tableRows_ = undefined; + + // Store this listener so it can be removed while updateContents_ modifies + // sortColumnIndex and sortDescending, then re-added. + this.sortColumnChangedListener_ = e => this.onSortColumnChanged_(e); + }, + + ready() { + this.$.table.zebra = true; + this.addEventListener('sort-column-changed', + this.sortColumnChangedListener_); + this.addEventListener('requestSelectionChange', + this.onRequestSelectionChange_.bind(this)); + this.addEventListener('row-expanded-changed', + this.onRowExpandedChanged_.bind(this)); + }, + + get viewState() { + return this.viewState_; + }, + + set viewState(vs) { + if (this.viewState_) { + throw new Error('viewState must be set exactly once.'); + } + this.viewState_ = vs; + this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this)); + // It would be arduous to construct a delta and call onViewStateUpdate_ + // here in case vs contains non-default values, so callers must set + // viewState first and then update it. + }, + + get histograms() { + return this.histograms_; + }, + + /** + * @param {!tr.v.HistogramSet} histograms + * @param {!tr.v.HistogramSet} sourceHistograms + * @param {!Array.<string>} displayLabels + * @param {function(string, function())=} opt_progress + */ + async build(histograms, sourceHistograms, displayLabels, opt_progress) { + this.histograms_ = histograms; + this.sourceHistograms_ = sourceHistograms; + this.filteredHistograms_ = undefined; + this.groupedHistograms_ = undefined; + this.displayLabels_ = displayLabels; + + if (opt_progress !== undefined) this.progress_ = opt_progress; + + if (histograms.length === 0) { + throw new Error('histogram-set-table requires non-empty HistogramSet.'); + } + + await this.progress_('Building columns...'); + this.$.table.tableColumns = [ + { + title: this.buildNameColumnTitle_(), + value: row => row.nameCell, + cmp: (a, b) => a.compareNames(b), + } + ].concat(displayLabels.map(l => this.buildColumn_(l))); + + tr.b.Timing.instant('histogram-set-table', 'columnCount', + this.$.table.tableColumns.length); + + // updateContents_() displays its own progress. + await this.updateContents_(); + + // Building some elements requires being able to measure them, which is + // impossible until they are displayed. If clients hide this table while + // it is being built, then they must display it when this event fires. + this.fire('display-ready'); + + this.progress_ = () => Promise.resolve(); + + this.checkNameColumnOverflow_( + tr.v.ui.HistogramSetTableRow.walkAll(this.$.table.tableRows)); + }, + + buildNameColumnTitle_() { + this.nameColumnTitle_ = document.createElement('span'); + this.nameColumnTitle_.style.display = 'inline-flex'; + + // Wrap the string in a span instead of using createTextNode() so that the + // span can be styled later. + const nameEl = document.createElement('span'); + nameEl.textContent = 'Name'; + this.nameColumnTitle_.appendChild(nameEl); + + const toggleWidthEl = document.createElement('span'); + toggleWidthEl.style.fontWeight = 'bold'; + toggleWidthEl.style.background = '#bbb'; + toggleWidthEl.style.color = '#333'; + toggleWidthEl.style.padding = '0px 3px'; + toggleWidthEl.style.marginRight = '8px'; + toggleWidthEl.style.display = 'none'; + toggleWidthEl.textContent = MIDLINE_HORIZONTAL_ELLIPSIS; + toggleWidthEl.addEventListener('click', + this.toggleNameColumnWidth_.bind(this)); + this.nameColumnTitle_.appendChild(toggleWidthEl); + return this.nameColumnTitle_; + }, + + toggleNameColumnWidth_(opt_event) { + this.viewState.update({ + constrainNameColumn: !this.viewState.constrainNameColumn, + }); + + if (opt_event !== undefined) { + opt_event.stopPropagation(); + opt_event.preventDefault(); + tr.b.Timing.instant('histogram-set-table', 'nameColumn' + + (this.viewState.constrainNameColumn ? 'Constrained' : + 'Unconstrained')); + } + }, + + buildColumn_(displayLabel) { + const title = document.createElement('span'); + title.textContent = displayLabel; + title.style.whiteSpace = 'pre'; + + return { + displayLabel, + title, + value: row => row.getCell(displayLabel), + cmp: (rowA, rowB) => rowA.compareCells(rowB, displayLabel), + }; + }, + + async updateContents_() { + const previousRowStates = this.viewState.tableRowStates; + + if (!this.filteredHistograms_) { + await this.progress_('Filtering rows...'); + this.filteredHistograms_ = this.viewState.showAll ? + this.histograms : this.sourceHistograms_; + + if (this.viewState.searchQuery) { + let query; + try { + query = new RegExp(this.viewState.searchQuery); + } catch (e) { + } + if (query !== undefined) { + this.filteredHistograms_ = new tr.v.HistogramSet( + [...this.filteredHistograms_].filter( + hist => hist.name.match(query))); + if (this.filteredHistograms_.length === 0 && + !this.viewState.showAll) { + await this.viewState.update({showAll: true}); + return; + } + } + } + this.groupedHistograms_ = undefined; + } + + if (!this.groupedHistograms_) { + await this.progress_('Grouping Histograms...'); + this.groupHistograms_(); + } + + if (!this.hierarchies_) { + await this.progress_('Merging Histograms...'); + this.hierarchies_ = tr.v.HistogramSetHierarchy.build( + this.groupedHistograms_); + this.tableRows_ = undefined; + } + + const tableRowsDirty = this.tableRows_ === undefined; + if (tableRowsDirty) { + // Wait to set this.$.table.tableRows until we're ready for it to build + // DOM. When tableRows are set on it, tr-ui-b-table calls + // setTimeout(..., 0) to schedule rebuild for the next interpreter tick, + // but that can happen in between the next await, which is too early. + this.tableRows_ = this.hierarchies_.map(hierarchy => + new tr.v.ui.HistogramSetTableRow( + hierarchy, this.$.table, this.viewState)); + + tr.b.Timing.instant('histogram-set-table', 'rootRowCount', + this.tableRows_.length); + + const namesToRowStates = new Map(); + for (const row of this.tableRows_) { + namesToRowStates.set(row.name, row.viewState); + } + await this.viewState.update({tableRowStates: namesToRowStates}); + } + + await this.progress_('Configuring table...'); + this.nameColumnTitle_.children[1].style.filter = + this.viewState.constrainNameColumn ? 'invert(100%)' : ''; + + const referenceDisplayLabelIndex = this.displayLabels_.indexOf( + this.viewState.referenceDisplayLabel); + this.$.table.selectedTableColumnIndex = (referenceDisplayLabelIndex < 0) ? + undefined : (1 + referenceDisplayLabelIndex); + + // Temporarily stop listening for this event in order to prevent the + // listener from updating viewState unnecessarily. + this.removeEventListener('sort-column-changed', + this.sortColumnChangedListener_); + this.$.table.sortColumnIndex = this.viewState.sortColumnIndex; + this.$.table.sortDescending = this.viewState.sortDescending; + this.addEventListener('sort-column-changed', + this.sortColumnChangedListener_); + + // Each name-cell listens to this.viewState for updates to + // constrainNameColumn. + // Each table-cell listens to this.viewState for updates to + // displayStatisticName and referenceDisplayLabel. + + if (tableRowsDirty) { + await this.progress_('Building DOM...'); + this.$.table.tableRows = this.tableRows_; + + // Try to restore previous row state. + // Wait to do this until after the base table has the new rows so that + // setExpandedForTableRow doesn't get confused. + for (const row of this.tableRows_) { + const previousState = previousRowStates.get(row.name); + if (!previousState) continue; + await row.restoreState(previousState); + } + } + + // It's always safe to call this, it will only recompute what is dirty. + // We want to make sure that the table is up to date when this async + // function resolves. + this.$.table.rebuild(); + }, + + async onRowExpandedChanged_(event) { + event.row.viewState.isExpanded = + this.$.table.getExpandedForTableRow(event.row); + tr.b.Timing.instant('histogram-set-table', + 'row' + (event.row.viewState.isExpanded ? 'Expanded' : 'Collapsed')); + + // When the user expands a row, the table builds subRows' name-cells. + // If a subRow's name isOverflowing even though none of the top-level rows + // are constrained, show the dots to allow the user to unconstrain the + // name column. + // Each name-cell.isOverflowing would force layout if we don't await + // animationFrame here, which would be inefficient. + if (this.nameColumnTitle_.children[1].style.display === 'block') return; + await tr.b.animationFrame(); + this.checkNameColumnOverflow_(event.row.subRows); + }, + + checkNameColumnOverflow_(rows) { + for (const row of rows) { + if (!row.nameCell.isOverflowing) continue; + + const [nameSpan, dots] = this.nameColumnTitle_.children; + dots.style.display = 'block'; + + // Size the span containing 'Name' so that the dots align with the + // ellipses in the name-cells. + const labelWidthPx = tr.v.ui.NAME_COLUMN_WIDTH_PX - + dots.getBoundingClientRect().width; + nameSpan.style.width = labelWidthPx + 'px'; + + return; + } + }, + + groupHistograms_() { + const groupings = this.viewState.groupings.slice(); + groupings.push(tr.v.HistogramGrouping.DISPLAY_LABEL); + + function canSkipGrouping(grouping, groupedHistograms) { + // Never skip meaningful groupings. + if (groupedHistograms.size > 1) return false; + + // Never skip the zero-th grouping. + if (grouping.key === groupings[0].key) return false; + + // Never skip the grouping that defines the table columns. + if (grouping.key === tr.v.HistogramGrouping.DISPLAY_LABEL.key) { + return false; + } + + // Skip meaningless groupings. + return true; + } + + this.groupedHistograms_ = + this.filteredHistograms_.groupHistogramsRecursively( + groupings, canSkipGrouping); + + this.hierarchies_ = undefined; + }, + + /** + * @param {!tr.b.Event} event + * @param {!Object} event.delta + * @param {!Object} event.delta.searchQuery + * @param {!Object} event.delta.referenceDisplayLabel + * @param {!Object} event.delta.displayStatisticName + * @param {!Object} event.delta.showAll + * @param {!Object} event.delta.groupings + * @param {!Object} event.delta.sortColumnIndex + * @param {!Object} event.delta.sortDescending + * @param {!Object} event.delta.constrainNameColumn + * @param {!Object} event.delta.tableRowStates + */ + async onViewStateUpdate_(event) { + if (this.histograms_ === undefined) return; + + if (event.delta.searchQuery !== undefined || + event.delta.showAll !== undefined) { + this.filteredHistograms_ = undefined; + } + + if (event.delta.groupings !== undefined) { + this.groupedHistograms_ = undefined; + } + + if (event.delta.displayStatistic !== undefined && + this.$.table.sortColumnIndex > 0) { + // Force re-sort. + this.$.table.sortColumnIndex = undefined; + } + + if (event.delta.referenceDisplayLabel !== undefined || + event.delta.displayStatisticName !== undefined) { + // Force this.$.table.bodyDirty_ = true; + this.$.table.tableRows = this.$.table.tableRows; + } + + // updateContents_() always copies sortColumnIndex and sortDescending + // from the viewState to the table. The table will only re-sort if + // they change. + + // Name-cells listen to this.viewState to handle updates to + // constrainNameColumn. + + if (event.delta.tableRowStates) { + if (this.tableRows_.length !== + this.viewState.tableRowStates.size) { + throw new Error( + 'Only histogram-set-table may update tableRowStates'); + } + for (const row of this.tableRows_) { + if (this.viewState.tableRowStates.get(row.name) !== row.viewState) { + throw new Error( + 'Only histogram-set-table may update tableRowStates'); + } + } + return; // No need to re-enter updateContents_(). + } + + await this.updateContents_(); + }, + + onSortColumnChanged_(event) { + tr.b.Timing.instant('histogram-set-table', 'sortColumn'); + this.viewState.update({ + sortColumnIndex: event.sortColumnIndex, + sortDescending: event.sortDescending, + }); + }, + + onRequestSelectionChange_(event) { + // This event may reference an EventSet or an array of Histogram names. + // If EventSet, let the BrushingStateController handle it. + if (event.selection instanceof tr.model.EventSet) return; + + event.stopPropagation(); + tr.b.Timing.instant('histogram-set-table', 'selectHistogramNames'); + + let histogramNames = event.selection; + histogramNames.sort(); + histogramNames = histogramNames.map(escapeRegExp).join('|'); + this.viewState.update({ + showAll: true, + searchQuery: `^(${histogramNames})$`, + }); + }, + + /** + * @return {!tr.v.HistogramSet} + */ + get leafHistograms() { + const histograms = new tr.v.HistogramSet(); + for (const row of + tr.v.ui.HistogramSetTableRow.walkAll(this.$.table.tableRows)) { + if (row.subRows.length) continue; + for (const hist of row.columns.values()) { + if (!(hist instanceof tr.v.Histogram)) continue; + + histograms.addHistogram(hist); + } + } + return histograms; + } + }); + + return { + MIDLINE_HORIZONTAL_ELLIPSIS, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html new file mode 100644 index 00000000000..8a1d158d021 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html @@ -0,0 +1,396 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/timing.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/ui/base/name_line_chart.html"> +<link rel="import" href="/tracing/value/ui/histogram_span.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id="tr-v-ui-histogram-set-table-cell"> + <template> + <style> + #histogram_container { + display: flex; + flex-direction: row; + } + + #missing, #empty, #unmergeable, #scalar { + flex-grow: 1; + } + + #open_histogram, #close_histogram, #open_histogram svg, #close_histogram svg { + height: 1em; + } + + #open_histogram svg { + margin-left: 4px; + stroke-width: 0; + stroke: blue; + fill: blue; + } + :host(:hover) #open_histogram svg { + background: blue; + stroke: white; + fill: white; + } + + #scalar { + flex-grow: 1; + white-space: nowrap; + } + + #histogram { + flex-grow: 1; + } + + #close_histogram svg line { + stroke-width: 18; + stroke: black; + } + #close_histogram:hover svg { + background: black; + } + #close_histogram:hover svg line { + stroke: white; + } + + #overview_container { + display: none; + } + </style> + + <div id="histogram_container"> + <span id="missing">(missing)</span> + <span id="empty">(empty)</span> + <span id="unmergeable">(unmergeable)</span> + + <tr-v-ui-scalar-span id="scalar" on-click="openHistogram_"></tr-v-ui-scalar-span> + + <span id="open_histogram" on-click="openHistogram_"> + <svg viewbox="0 0 128 128"> + <rect x="16" y="24" width="32" height="16"/> + <rect x="16" y="56" width="96" height="16"/> + <rect x="16" y="88" width="64" height="16"/> + </svg> + </span> + + <span id="histogram"></span> + + <span id="close_histogram" on-click="closeHistogram_"> + <svg viewbox="0 0 128 128"> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </svg> + </span> + </div> + + <div id="overview_container"> + </div> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-histogram-set-table-cell', + + created() { + this.viewState_ = undefined; + this.rootListener_ = this.onRootStateUpdate_.bind(this); + this.row_ = undefined; + this.displayLabel_ = ''; + this.histogram_ = undefined; + this.histogramSpan_ = undefined; + this.overviewChart_ = undefined; + this.mwuResult_ = undefined; + }, + + ready() { + this.addEventListener('click', this.onClick_.bind(this)); + }, + + attached() { + if (this.row) { + this.row.rootViewState.addUpdateListener(this.rootListener_); + } + }, + + detached() { + this.row.rootViewState.removeUpdateListener(this.rootListener_); + // Don't need to removeUpdateListener for the row and cells; their + // lifetimes are the same as |this|. + }, + + updateMwu_() { + const referenceHistogram = this.referenceHistogram; + this.mwuResult_ = undefined; + if (!(this.histogram instanceof tr.v.Histogram)) return; + if (!this.histogram.canCompare(referenceHistogram)) return; + this.mwuResult_ = tr.b.math.Statistics.mwu( + this.histogram.sampleValues, + referenceHistogram.sampleValues, + this.row.rootViewState.alpha); + }, + + build(row, displayLabel, viewState) { + this.row_ = row; + this.displayLabel_ = displayLabel; + this.viewState_ = viewState; + this.histogram_ = this.row.columns.get(displayLabel); + + if (this.viewState) { + // this.viewState is undefined when this.histogram_ is undefined. + // In that case, onViewStateUpdate_ wouldn't be able to do anything + // anyway. + this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this)); + } + this.row.viewState.addUpdateListener(this.onRowStateUpdate_.bind(this)); + if (this.isAttached) { + this.row.rootViewState.addUpdateListener(this.rootListener_); + } + + this.updateMwu_(); + + // this.histogram_ and this.referenceHistogram might be undefined, + // a HistogramSet of unmergeable Histograms, or a Histogram. + this.updateContents_(); + }, + + updateSignificance_() { + if (!this.mwuResult_) return; + this.$.scalar.significance = this.mwuResult_.significance; + }, + + get viewState() { + return this.viewState_; + }, + + get row() { + return this.row_; + }, + + get histogram() { + return this.histogram_; + }, + + get referenceHistogram() { + const referenceDisplayLabel = + this.row.rootViewState.referenceDisplayLabel; + if (!referenceDisplayLabel) return undefined; + if (referenceDisplayLabel === this.displayLabel_) return undefined; + return this.row.columns.get(referenceDisplayLabel); + }, + + get isHistogramOpen() { + return (this.histogramSpan_ !== undefined) && + (this.$.histogram.style.display === 'block'); + }, + + set isHistogramOpen(open) { + if (!(this.histogram instanceof tr.v.Histogram) || + (this.histogram.numValues === 0)) { + return; + } + + // Unfortunately, we can't use a css attribute for this since this stuff + // is tied up in all the possible states of this.histogram. See + // updateContents_(). + + this.$.scalar.style.display = open ? 'none' : 'flex'; + this.$.open_histogram.style.display = open ? 'none' : 'block'; + + this.$.close_histogram.style.display = open ? 'block' : 'none'; + this.$.histogram.style.display = open ? 'block' : 'none'; + + // Wait to create the histogram-span until the user wants to display it + // in order to speed up creating lots of histogram-set-table-cells when + // building the table. + if (open && this.histogramSpan_ === undefined) { + this.histogramSpan_ = document.createElement('tr-v-ui-histogram-span'); + this.histogramSpan_.viewState = this.viewState; + this.histogramSpan_.rowState = this.row.viewState; + this.histogramSpan_.rootState = this.row.rootViewState; + this.histogramSpan_.build(this.histogram, this.referenceHistogram); + this.$.histogram.appendChild(this.histogramSpan_); + } + + this.viewState.isOpen = open; + }, + + onViewStateUpdate_(event) { + if (event.delta.isOpen) { + this.isHistogramOpen = this.viewState.isOpen; + } + }, + + onRowStateUpdate_(event) { + if (event.delta.isOverviewed === undefined) return; + if (this.row.viewState.isOverviewed) { + this.showOverview(); + } else { + this.hideOverview(); + } + }, + + onRootStateUpdate_(event) { + if (event.delta.referenceDisplayLabel && + this.histogramSpan_) { + this.histogramSpan_.build(this.histogram, this.referenceHistogram); + } + + if (event.delta.displayStatisticName || + event.delta.referenceDisplayLabel) { + this.updateMwu_(); + this.updateContents_(); + } else if (event.delta.alpha && this.mwuResult_) { + this.mwuResult_.compare(this.row.rootViewState.alpha); + this.updateSignificance_(); + } + + if (this.row.viewState.isOverviewed && + (event.delta.sortColumnIndex || + event.delta.sortDescending || + event.delta.displayStatisticName || + event.delta.referenceDisplayLabel)) { + if (this.overviewChart_ !== undefined) { + this.$.overview_container.removeChild(this.overviewChart_); + this.overviewChart_ = undefined; + } + this.showOverview(); + } + }, + + onClick_(event) { + // Since the histogram-set-table's table doesn't support any kind of + // selection, clicking anywhere within a row that has subRows will + // expand/collapse that row, which can relayout the table and move things + // around. Prevent table relayout by preventing the tr-ui-b-table from + // receiving the click event. + event.stopPropagation(); + }, + + openHistogram_() { + this.isHistogramOpen = true; + tr.b.Timing.instant('histogram-set-table-cell', 'open'); + }, + + closeHistogram_() { + this.isHistogramOpen = false; + tr.b.Timing.instant('histogram-set-table-cell', 'close'); + }, + + updateContents_() { + const isOpen = this.isHistogramOpen; + + this.$.empty.style.display = 'none'; + this.$.unmergeable.style.display = 'none'; + this.$.scalar.style.display = 'none'; + this.$.histogram.style.display = 'none'; + this.$.close_histogram.style.display = 'none'; + this.$.open_histogram.style.visibility = 'hidden'; + + if (!this.histogram) { + this.$.missing.style.display = 'block'; + return; + } + + this.$.missing.style.display = 'none'; + + if (this.histogram instanceof tr.v.HistogramSet) { + this.$.unmergeable.style.display = 'block'; + return; + } + + if (!(this.histogram instanceof tr.v.Histogram)) { + throw new Error('Invalid Histogram: ' + this.histogram); + } + + if (this.histogram.numValues === 0) { + this.$.empty.style.display = 'block'; + return; + } + + this.$.open_histogram.style.display = 'block'; + this.$.open_histogram.style.visibility = 'visible'; + this.$.scalar.style.display = 'flex'; + + this.updateSignificance_(); + + const referenceHistogram = this.referenceHistogram; + const statName = this.histogram.getAvailableStatisticName( + this.row.rootViewState.displayStatisticName, referenceHistogram); + const statisticScalar = this.histogram.getStatisticScalar( + statName, referenceHistogram); + this.$.scalar.setValueAndUnit( + statisticScalar.value, statisticScalar.unit); + + this.isHistogramOpen = isOpen; + }, + + showOverview() { + this.$.overview_container.style.display = 'block'; + if (this.overviewChart_ !== undefined) return; + + this.row.sortSubRows(); + let referenceDisplayLabel = + this.row.rootViewState.referenceDisplayLabel; + if (referenceDisplayLabel === this.displayLabel_) { + referenceDisplayLabel = undefined; + } + const displayStatisticName = this.row.rootViewState.displayStatisticName; + const data = []; + let unit; + + for (const subRow of this.row.subRows) { + const subHist = subRow.columns.get(this.displayLabel_); + if (!(subHist instanceof tr.v.Histogram)) continue; + + if (unit === undefined) { + unit = subHist.unit; + } else if (unit !== subHist.unit) { + // The subrows have different units, so the overview chart cannot + // use a single unit to format all of the values, so don't display + // an overview chart at all. + data.splice(0); + break; + } + + const refHist = subRow.columns.get(referenceDisplayLabel); + const statName = subHist.getAvailableStatisticName( + displayStatisticName, refHist); + const statScalar = subHist.getStatisticScalar( + statName, refHist); + + if (statScalar !== undefined) { + data.push({ + x: subRow.name, + y: statScalar.value, + }); + } + } + if (data.length < 2) return; + + this.overviewChart_ = new tr.ui.b.NameLineChart(); + this.$.overview_container.appendChild(this.overviewChart_); + this.overviewChart_.displayXInHover = true; + this.overviewChart_.hideLegend = true; + this.overviewChart_.unit = unit; + this.overviewChart_.overrideDataRange = this.row.overviewDataRange; + this.overviewChart_.data = data; + }, + + hideOverview() { + this.$.overview_container.style.display = 'none'; + } + }); + + return { + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html new file mode 100644 index 00000000000..f0dec062018 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html @@ -0,0 +1,361 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/timing.html"> +<link rel="import" href="/tracing/ui/base/name_line_chart.html"> + +<dom-module id="tr-v-ui-histogram-set-table-name-cell"> + <template> + <style> + #name_container { + display: flex; + } + + #name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + #show_overview, #hide_overview, #show_overview svg, #hide_overview svg { + height: 1em; + margin-left: 5px; + } + + #show_overview svg { + stroke: blue; + stroke-width: 16; + } + + #show_overview:hover svg { + background: blue; + stroke: white; + } + + #hide_overview { + display: none; + } + + #hide_overview svg { + stroke-width: 18; + stroke: black; + } + + #hide_overview:hover svg { + background: black; + stroke: white; + } + + #open_histograms, #close_histograms, #open_histograms svg, #close_histograms svg { + height: 1em; + } + + #close_histograms { + display: none; + } + + #open_histograms svg { + margin-left: 4px; + stroke-width: 0; + stroke: blue; + fill: blue; + } + #open_histograms:hover svg { + background: blue; + stroke: white; + fill: white; + } + + #close_histograms line { + stroke-width: 18; + stroke: black; + } + #close_histograms:hover { + background: black; + } + #close_histograms:hover line { + stroke: white; + } + + #overview_container { + display: none; + } + </style> + + <div id="name_container"> + <span id="name"></span> + + <span id="show_overview" on-click="showOverview_"> + <svg viewbox="0 0 128 128"> + <line x1="19" y1="109" x2="49" y2="49"/> + <line x1="49" y1="49" x2="79" y2="79"/> + <line x1="79" y1="79" x2="109" y2="19"/> + </svg> + </span> + + <span id="hide_overview" on-click="hideOverview_"> + <svg viewbox="0 0 128 128"> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </svg> + </span> + + <span id="open_histograms" on-click="openHistograms_"> + <svg viewbox="0 0 128 128"> + <rect x="16" y="24" width="32" height="16"/> + <rect x="16" y="56" width="96" height="16"/> + <rect x="16" y="88" width="64" height="16"/> + </svg> + </span> + + <span id="close_histograms" on-click="closeHistograms_"> + <svg viewbox="0 0 128 128"> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </svg> + </span> + </div> + + <div id="overview_container"> + </div> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + const NAME_COLUMN_WIDTH_PX = 300; + + Polymer({ + is: 'tr-v-ui-histogram-set-table-name-cell', + + created() { + this.row_ = undefined; + this.overviewChart_ = undefined; + this.cellListener_ = this.onCellStateUpdate_.bind(this); + this.rootListener_ = this.onRootStateUpdate_.bind(this); + }, + + attached() { + if (this.row) { + this.row.rootViewState.addUpdateListener(this.rootListener_); + } + }, + + detached() { + this.row.rootViewState.removeUpdateListener(this.rootListener_); + // Don't need to removeUpdateListener for the row and cells; their + // lifetimes are the same as |this|. + }, + + get row() { + return this.row_; + }, + + build(row) { + if (this.row_ !== undefined) { + throw new Error('row must be set exactly once.'); + } + this.row_ = row; + this.row.viewState.addUpdateListener(this.onRowStateUpdate_.bind(this)); + this.constrainWidth = this.row.rootViewState.constrainNameColumn; + if (this.isAttached) { + this.row.rootViewState.addUpdateListener(this.rootListener_); + } + + for (const cellState of this.row.viewState.cells.values()) { + cellState.addUpdateListener(this.cellListener_); + } + + Polymer.dom(this.$.name).textContent = this.row.name; + + this.title = this.row.name; + if (this.row.description) { + this.title += '\n' + this.row.description; + } + + if (this.row.overviewDataRange.isEmpty || + this.row.overviewDataRange.min === this.row.overviewDataRange.max) { + // TODO(#3744) Also hide this button when column or subrow units don't + // match. + this.$.show_overview.style.display = 'none'; + } + + let histogramCount = 0; + for (const cell of this.row.columns.values()) { + if (cell instanceof tr.v.Histogram && + cell.numValues > 0) { + ++histogramCount; + } + } + if (histogramCount <= 1) { + this.$.open_histograms.style.display = 'none'; + } + }, + + set constrainWidth(constrain) { + this.$.name.style.maxWidth = constrain ? + (this.nameWidthPx + 'px') : 'none'; + }, + + get nameWidthPx() { + // tr-ui-b-table adds 16px of padding for each additional level of subRows + // nesting, so outer nameDivs can be wider than inner nameDivs. + return NAME_COLUMN_WIDTH_PX - (16 * this.row.depth); + }, + + get isOverflowing() { + return this.$.name.style.maxWidth !== 'none' && + this.$.name.getBoundingClientRect().width === this.nameWidthPx; + }, + + get isOverviewed() { + return this.$.overview_container.style.display === 'block'; + }, + + set isOverviewed(isOverviewed) { + if (isOverviewed === this.isOverviewed) return; + if (isOverviewed) { + this.showOverview_(); + } else { + this.hideOverview_(); + } + }, + + hideOverview_(opt_event) { + this.$.overview_container.style.display = 'none'; + this.$.hide_overview.style.display = 'none'; + this.$.show_overview.style.display = 'block'; + + if (opt_event !== undefined) { + opt_event.stopPropagation(); + tr.b.Timing.instant('histogram-set-table-name-cell', 'hideOverview'); + this.row.viewState.isOverviewed = this.isOverviewed; + } + }, + + showOverview_(opt_event) { + if (opt_event !== undefined) { + opt_event.stopPropagation(); + tr.b.Timing.instant('histogram-set-table-name-cell', 'showOverview'); + this.row.viewState.isOverviewed = true; + } + + this.$.overview_container.style.display = 'block'; + this.$.hide_overview.style.display = 'block'; + this.$.show_overview.style.display = 'none'; + + if (this.overviewChart_ === undefined) { + const displayStatisticName = + this.row.rootViewState.displayStatisticName; + const data = []; + let unit; + + for (const [displayLabel, hist] of this.row.sortedColumns()) { + if (!(hist instanceof tr.v.Histogram)) continue; + + if (unit === undefined) { + unit = hist.unit; + } else if (unit !== hist.unit) { + // The columns have different units, so the overview chart cannot + // use a single unit to format all of the values, so don't display + // an overview chart at all. + data.splice(0); + break; + } + + const statName = hist.getAvailableStatisticName(displayStatisticName); + const statScalar = hist.getStatisticScalar(statName); + + if (statScalar !== undefined) { + data.push({ + x: displayLabel, + y: statScalar.value, + }); + } + } + if (data.length < 2) { + return; + } + + this.overviewChart_ = new tr.ui.b.NameLineChart(); + this.$.overview_container.appendChild(this.overviewChart_); + this.overviewChart_.displayXInHover = true; + this.overviewChart_.hideLegend = true; + this.overviewChart_.unit = unit; + this.overviewChart_.overrideDataRange = this.row.overviewDataRange; + this.overviewChart_.data = data; + } + }, + + openHistograms_(event) { + event.stopPropagation(); + tr.b.Timing.instant('histogram-set-table-name-cell', 'openHistograms'); + for (const cell of this.row.cells.values()) { + cell.isHistogramOpen = true; + } + this.$.close_histograms.style.display = 'block'; + this.$.open_histograms.style.display = 'none'; + }, + + closeHistograms_(event) { + event.stopPropagation(); + tr.b.Timing.instant('histogram-set-table-name-cell', 'closeHistograms'); + for (const cell of this.row.cells.values()) { + cell.isHistogramOpen = false; + } + this.$.open_histograms.style.display = 'block'; + this.$.close_histograms.style.display = 'none'; + }, + + onRootStateUpdate_(event) { + if (event.delta.constrainNameColumn) { + this.constrainWidth = this.row.rootViewState.constrainNameColumn; + } + if (this.row.viewState.isOverviewed && + event.delta.displayStatisticName) { + this.row.resetOverviewDataRange(); + if (this.overviewChart_ !== undefined) { + this.$.overview_container.removeChild(this.overviewChart_); + this.overviewChart_ = undefined; + } + this.showOverview_(); + } + }, + + onRowStateUpdate_(event) { + if (event.delta.isOverviewed) { + this.isOverviewed = this.row.viewState.isOverviewed; + } + // This assumes that cell states are not updated. + }, + + onCellStateUpdate_(event) { + if (!event.delta.isOpen) return; + + let cellCount = 0; + let openCellCount = 0; + for (const cell of this.row.cells.values()) { + if (!(cell.histogram instanceof tr.v.Histogram) || + (cell.histogram.numValues === 0)) { + continue; + } + ++cellCount; + if (cell.isHistogramOpen) ++openCellCount; + } + if (cellCount <= 1) return; + const mostlyOpen = openCellCount > (cellCount / 2); + this.$.open_histograms.style.display = mostlyOpen ? 'none' : 'block'; + this.$.close_histograms.style.display = mostlyOpen ? 'block' : 'none'; + } + }); + + return { + NAME_COLUMN_WIDTH_PX, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html new file mode 100644 index 00000000000..b4cb1a54020 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html @@ -0,0 +1,299 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/ui/histogram_set_table_cell.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_table_name_cell.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + class HistogramSetTableRow { + /** + * @param {!tr.v.HistogramSetHierarchy} hierarchy + * @param {!Element} baseTable tr-ui-b-table + * @param {!tr.v.ui.HistogramSetViewState} rootViewState + */ + constructor(hierarchy, baseTable, rootViewState) { + this.hierarchy_ = hierarchy; + this.baseTable_ = baseTable; + this.rootViewState_ = rootViewState; + this.viewState_ = new tr.v.ui.HistogramSetTableRowState(); + this.viewState_.addUpdateListener(this.onViewStateUpdate_.bind(this)); + this.overviewDataRange_ = undefined; + this.nameCell_ = undefined; + this.cells_ = new Map(); + this.subRows_ = []; + + // Don't assign viewState.subRows or cells. There can't be anything + // listening to viewState, so avoid the overhead of dispatching an event. + for (const subHierarchy of hierarchy.subRows) { + const subRow = new HistogramSetTableRow( + subHierarchy, baseTable, rootViewState); + this.subRows_.push(subRow); + this.viewState.subRows.set(subRow.name, subRow.viewState); + } + for (const columnName of this.columns.keys()) { + this.viewState.cells.set( + columnName, new tr.v.ui.HistogramSetTableCellState()); + } + } + + /** + * @return {string} + */ + get name() { + return this.hierarchy_.name; + } + + /** + * @return {number} + */ + get depth() { + return this.hierarchy_.depth; + } + + /** + * @return {string} + */ + get description() { + return this.hierarchy_.description; + } + + /** + * @return {!Map.<string, !(undefined|tr.v.Histogram|tr.v.HistogramSet)>} + */ + get columns() { + return this.hierarchy_.columns; + } + + * sortedColumns() { + for (const col of this.baseTable_.tableColumns) { + yield [ + col.displayLabel, + this.hierarchy_.columns.get(col.displayLabel), + ]; + } + } + + /** + * @return {!tr.b.Range} + */ + get overviewDataRange() { + if (this.overviewDataRange_ === undefined) { + this.overviewDataRange_ = new tr.b.math.Range(); + + const displayStatisticName = + this.rootViewState.displayStatisticName; + const referenceDisplayLabel = + this.rootViewState.referenceDisplayLabel; + + for (const [displayLabel, hist] of this.columns) { + if (hist instanceof tr.v.Histogram) { + const statName = hist.getAvailableStatisticName( + displayStatisticName); + const statScalar = hist.getStatisticScalar(statName); + if (statScalar !== undefined) { + this.overviewDataRange_.addValue(statScalar.value); + } + } + + for (const subRow of this.subRows) { + const subHist = subRow.columns.get(displayLabel); + if (!(subHist instanceof tr.v.Histogram)) continue; + + const refHist = subRow.columns.get(referenceDisplayLabel); + const statName = subHist.getAvailableStatisticName( + displayStatisticName, refHist); + const statScalar = subHist.getStatisticScalar( + statName, refHist); + + if (statScalar !== undefined) { + this.overviewDataRange_.addValue(statScalar.value); + } + } + } + } + return this.overviewDataRange_; + } + + /** + * overviewDataRange is used by histogram-set-table-cell (hstc) and + * histogram-set-table-name-cell (hstnc) to display overview line charts + * with consistent y-axes. + * overviewDataRange depends on HistogramSetViewState.displayStatisticName + * and referenceDisplayLabel, so it must be recomputed when either of those + * changes. + * overviewDataRange should not be recomputed for each hstc in the row; it + * should only be computed once when necessary, and cached. + * HistogramSetTableRow (HSTR) cannot listen to HistogramSetViewState + * (HSVS) updates because there is no way for it to remove the listener. + * However, Polymer has detached callbacks, so dom-modules can listen to + * HSVS updates without leaking memory. + * overviewDataRange should be recomputed only once whenever + * displayStatisticName or referenceDisplayLabel changes. + * There is exactly one hstnc per row. + * histogram-set-table-name-cell resets overviewDataRange when + * displayStatisticName or referenceDisplayLabel changes. + */ + resetOverviewDataRange() { + this.overviewDataRange_ = undefined; + } + + /** + * @return {!tr.v.ui.HistogramSetViewState} + */ + get rootViewState() { + return this.rootViewState_; + } + + /** + * @return {!Map.<string, !Element>} tr-v-ui-histogram-set-table-cell + */ + get cells() { + return this.cells_; + } + + /** + * @return {!Array.<tr.v.ui.HistogramSetTableRow>} + */ + get subRows() { + return this.subRows_; + } + + /** + * @return {!Array.<tr.v.ui.HistogramSetTableRowState>} + */ + get viewState() { + return this.viewState_; + } + + * walk() { + yield this; + for (const row of this.subRows) yield* row.walk(); + } + + static* walkAll(rootRows) { + for (const rootRow of rootRows) yield* rootRow.walk(); + } + + get nameCell() { + if (this.nameCell_ === undefined) { + this.nameCell_ = document.createElement( + 'tr-v-ui-histogram-set-table-name-cell'); + this.nameCell_.build(this); + } + return this.nameCell_; + } + + getCell(columnName) { + if (this.cells.has(columnName)) return this.cells.get(columnName); + const cell = document.createElement('tr-v-ui-histogram-set-table-cell'); + cell.build(this, columnName, this.viewState.cells.get(columnName)); + this.cells.set(columnName, cell); + return cell; + } + + compareNames(other) { + return this.name.localeCompare(other.name); + } + + compareCells(other, displayLabel) { + // If a reference column is selected, compare the absolute deltas + // between the two cells and their references. + const referenceDisplayLabel = this.rootViewState.referenceDisplayLabel; + let referenceCellA; + let referenceCellB; + if (referenceDisplayLabel && + referenceDisplayLabel !== displayLabel) { + referenceCellA = this.columns.get(referenceDisplayLabel); + referenceCellB = other.columns.get(referenceDisplayLabel); + } + + const cellA = this.columns.get(displayLabel); + let valueA = 0; + if (cellA instanceof tr.v.Histogram) { + const statisticA = cellA.getAvailableStatisticName( + this.rootViewState.displayStatisticName, referenceCellA); + const scalarA = cellA.getStatisticScalar(statisticA, referenceCellA); + if (scalarA) { + valueA = scalarA.value; + } + } + + const cellB = other.columns.get(displayLabel); + let valueB = 0; + if (cellB instanceof tr.v.Histogram) { + const statisticB = cellB.getAvailableStatisticName( + this.rootViewState.displayStatisticName, referenceCellB); + const scalarB = cellB.getStatisticScalar(statisticB, referenceCellB); + if (scalarB) { + valueB = scalarB.value; + } + } + + return valueA - valueB; + } + + onViewStateUpdate_(event) { + if (event.delta.isExpanded) { + this.baseTable_.setExpandedForTableRow(this, this.viewState.isExpanded); + } + + if (event.delta.subRows) { + throw new Error('HistogramSetTableRow.subRows must not be reassigned.'); + } + + if (event.delta.cells) { + // Only validate the cells that have already been built. + // Cells may not have been built yet, so only validate the cells that + // have been built. + for (const [displayLabel, cell] of this.cells) { + if (cell.viewState !== this.viewState.cells.get(displayLabel)) { + throw new Error('Only HistogramSetTableRow may update cells'); + } + } + } + } + + async restoreState(vs) { + // Don't use updateFromViewState() because it would overwrite cells and + // subRows, but we just want to restore them. + await this.viewState.update({ + isExpanded: vs.isExpanded, + isOverviewed: vs.isOverviewed, + }); + + // If cells haven't been built yet, then their state will be restored when + // they are built. + for (const [displayLabel, cell] of this.cells) { + const previousState = vs.cells.get(displayLabel); + if (!previousState) continue; + await cell.viewState.updateFromViewState(previousState); + } + for (const row of this.subRows) { + const previousState = vs.subRows.get(row.name); + if (!previousState) continue; + await row.restoreState(previousState); + } + } + + sortSubRows() { + const sortColumn = this.baseTable_.tableColumns[ + this.rootViewState_.sortColumnIndex]; + if (sortColumn === undefined) return; + this.subRows_.sort(sortColumn.cmp); + if (this.rootViewState_.sortDescending) { + this.subRows_.reverse(); + } + } + } + + return { + HistogramSetTableRow, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html new file mode 100644 index 00000000000..f8d76afc72b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html @@ -0,0 +1,1679 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/assert_utils.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(function() { + // TODO(#3811) Clean up these tests. + + const TEST_BOUNDARIES = tr.v.HistogramBinBoundaries.createLinear(0, 1e3, 20); + + async function buildTable(test, histograms) { + // This should mirror HistogramImporter in order to be as similar to + // results.html as possible. + const table = document.createElement('tr-v-ui-histogram-set-table'); + + table.viewState = new tr.v.ui.HistogramSetViewState(); + await table.viewState.update({ + displayStatisticName: 'avg', + groupings: [tr.v.HistogramGrouping.HISTOGRAM_NAME], + }); + + table.style.display = 'none'; + test.addHTMLOutput(table); + + table.addEventListener('display-ready', () => { + table.style.display = ''; + }); + + const collector = new tr.v.HistogramParameterCollector(); + collector.process(histograms); + + await table.build( + histograms, + histograms.sourceHistograms, + collector.labels, + async message => { + await tr.b.animationFrame(); + }); + return table; + } + + function range(start, end) { + const result = []; + for (let i = start; i < end; ++i) result.push(i); + return result; + } + + function getBaseTable(table) { + return tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.tagName === 'TR-UI-B-TABLE'); + } + + function getNameCells(table) { + return tr.ui.b.findDeepElementsMatchingPredicate(table, e => + e.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-NAME-CELL'); + } + + function getTableCells(table) { + return tr.ui.b.findDeepElementsMatchingPredicate(table, e => + e.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL'); + } + + test('viewSearchQuery', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2]); + const table = await buildTable(this, histograms); + + await table.viewState.update({searchQuery: 'a'}); + let cells = getTableCells(table); + assert.lengthOf(cells, 1); + + await table.viewState.update({searchQuery: '[z-'}); + cells = getTableCells(table); + assert.lengthOf(cells, 2); + + await table.viewState.update({searchQuery: 'x'}); + cells = getTableCells(table); + assert.lengthOf(cells, 0); + + await table.viewState.update({searchQuery: ''}); + cells = getTableCells(table); + assert.lengthOf(cells, 2); + }); + + test('controlSearchQuery', async function() { + const histograms = new tr.v.HistogramSet(); + const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, + {value: 1, diagnostics: {r: tr.v.d.Breakdown.fromEntries([['0', 1]])}}); + const bHist = histograms.createHistogram('b', tr.b.Unit.byName.count, []); + const related = new tr.v.d.RelatedNameMap(); + related.set('0', bHist.name); + aHist.diagnostics.set('r', related); + const table = await buildTable(this, histograms); + await table.viewState.tableRowStates.get('a').cells.get('Value').update( + {isOpen: true}); + const link = tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.tagName === 'TR-UI-A-ANALYSIS-LINK'); + link.click(); + assert.strictEqual('^(b)$', table.viewState.searchQuery); + }); + + test('viewReferenceDisplayLabel', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1], { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']) + ]]), + }); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2], { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']) + ]]), + }); + const table = await buildTable(this, histograms); + const baseTable = getBaseTable(table); + assert.isUndefined(baseTable.selectedTableColumnIndex); + + await table.viewState.update({referenceDisplayLabel: 'A'}); + assert.strictEqual(1, baseTable.selectedTableColumnIndex); + + await table.viewState.update({referenceDisplayLabel: 'B'}); + assert.strictEqual(2, baseTable.selectedTableColumnIndex); + }); + + test('viewDisplayStatisticName', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, range(0, 10), { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']) + ]]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, range(10, 20), { + diagnostics: new Map([[ + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']) + ]]), + }); + const table = await buildTable(this, histograms); + let scalarSpans = tr.ui.b.findDeepElementsMatchingPredicate(table, e => + e.tagName === 'TR-V-UI-SCALAR-SPAN'); + assert.lengthOf(scalarSpans, 2); + assert.strictEqual('5', scalarSpans[0].unit.format(scalarSpans[0].value)); + assert.strictEqual('15', scalarSpans[1].unit.format(scalarSpans[1].value)); + + await table.viewState.update({displayStatisticName: 'std'}); + scalarSpans = tr.ui.b.findDeepElementsMatchingPredicate(table, e => + e.tagName === 'TR-V-UI-SCALAR-SPAN'); + assert.lengthOf(scalarSpans, 2); + assert.strictEqual('3', scalarSpans[0].unit.format(scalarSpans[0].value)); + assert.strictEqual('3', scalarSpans[1].unit.format(scalarSpans[1].value)); + }); + + test('autoShowAll', async function() { + const histograms = new tr.v.HistogramSet(); + const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + const bHist = histograms.createHistogram('b', tr.b.Unit.byName.count, []); + const related = new tr.v.d.RelatedNameMap(); + related.set('0', bHist.name); + aHist.diagnostics.set('r', related); + const table = await buildTable(this, histograms); + + let cells = getNameCells(table); + assert.lengthOf(cells, 2); + assert.strictEqual('a', cells[0].row.name); + + await table.viewState.update({searchQuery: 'b'}); + assert.isTrue(table.viewState.showAll); + cells = getNameCells(table); + assert.lengthOf(cells, 1); + assert.strictEqual('b', cells[0].row.name); + }); + + test('viewShowAll', async function() { + const histograms = new tr.v.HistogramSet(); + const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + const bHist = histograms.createHistogram('b', tr.b.Unit.byName.count, []); + const related = new tr.v.d.RelatedNameMap(); + related.set('0', bHist.name); + aHist.diagnostics.set('r', related); + const table = await buildTable(this, histograms); + + let cells = getNameCells(table); + assert.lengthOf(cells, 2); + assert.strictEqual('a', cells[0].row.name); + assert.strictEqual('b', cells[1].row.name); + + await table.viewState.update({showAll: false}); + cells = getNameCells(table); + assert.lengthOf(cells, 1); + assert.strictEqual('a', cells[0].row.name); + }); + + test('viewSortColumnIndex', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2]); + const table = await buildTable(this, histograms); + const baseTable = getBaseTable(table); + assert.strictEqual(baseTable.sortColumnIndex, 0); + assert.isFalse(baseTable.sortDescending); + + await table.viewState.update({sortColumnIndex: 1, sortDescending: true}); + assert.isTrue(baseTable.sortDescending); + assert.strictEqual(baseTable.sortColumnIndex, 1); + }); + + test('controlSortColumnIndex', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2]); + const table = await buildTable(this, histograms); + + assert.strictEqual(0, table.viewState.sortColumnIndex); + + tr.ui.b.findDeepElementsMatchingPredicate( + table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[0].click(); + assert.strictEqual(0, table.viewState.sortColumnIndex); + + tr.ui.b.findDeepElementsMatchingPredicate( + table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[1].click(); + assert.strictEqual(1, table.viewState.sortColumnIndex); + }); + + test('viewSortDescending', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2]); + const table = await buildTable(this, histograms); + + await table.viewState.update({sortColumnIndex: 0}); + + await table.viewState.update({sortDescending: true}); + + await table.viewState.update({sortDescending: false}); + }); + + test('controlSortDescending', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2]); + const table = await buildTable(this, histograms); + await table.viewState.update({sortColumnIndex: 0}); + + assert.isFalse(table.viewState.sortDescending); + + tr.ui.b.findDeepElementsMatchingPredicate( + table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[0].click(); + assert.isTrue(table.viewState.sortDescending); + + tr.ui.b.findDeepElementsMatchingPredicate( + table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[0].click(); + assert.isFalse(table.viewState.sortDescending); + }); + + test('sortUndefinedStatistics', async function() { + // The 'avg' statistic Scalar of an empty histogram is undefined, so + // HistogramSetTableRow.compareCells must not throw when it encounters + // undefined Scalars. + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + histograms.createHistogram('b', tr.b.Unit.byName.count, []); + const table = await buildTable(this, histograms); + await table.viewState.update({sortColumnIndex: 1}); + }); + + test('sortByDeltaStatistic', async function() { + const histograms0 = new tr.v.HistogramSet(); + const histograms1 = new tr.v.HistogramSet(); + const a0Hist = histograms0.createHistogram( + 'a', tr.b.Unit.byName.count, [0]); + const b0Hist = histograms0.createHistogram( + 'b', tr.b.Unit.byName.count, [0]); + const c0Hist = histograms0.createHistogram( + 'c', tr.b.Unit.byName.count, [3]); + const a1Hist = histograms1.createHistogram( + 'a', tr.b.Unit.byName.count, [1]); + const b1Hist = histograms1.createHistogram( + 'b', tr.b.Unit.byName.count, [2]); + const c1Hist = histograms1.createHistogram( + 'c', tr.b.Unit.byName.count, [3]); + histograms0.addSharedDiagnosticToAllHistograms( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['L0'])); + histograms0.addSharedDiagnosticToAllHistograms( + tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(0)); + histograms1.addSharedDiagnosticToAllHistograms( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['L1'])); + histograms1.addSharedDiagnosticToAllHistograms( + tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(1)); + + const table = await buildTable(this, new tr.v.HistogramSet( + Array.from(histograms0).concat(Array.from(histograms1)))); + await table.viewState.update({ + displayStatisticName: tr.v.DELTA + 'avg', + referenceDisplayLabel: 'L0', + sortColumnIndex: 2, + }); + const nameCells = getNameCells(table); + assert.strictEqual('c', nameCells[0].row.name); + assert.strictEqual('a', nameCells[1].row.name); + assert.strictEqual('b', nameCells[2].row.name); + }); + + test('sortMissing', async function() { + // Missing cells should be treated as zero for sorting purposes. The + // comparator must not return undefined or NaN. + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['x'])], + ]), + }); + histograms.createHistogram('b', tr.b.Unit.byName.count, [2], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['x'])], + ]), + }); + // 'c','x' intentionally missing + histograms.createHistogram('a', tr.b.Unit.byName.count, [1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['y'])], + ]), + }); + // 'b','y' intentionally missing + histograms.createHistogram('c', tr.b.Unit.byName.count, [-1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['y'])], + ]), + }); + const table = await buildTable(this, histograms); + await table.viewState.update({sortColumnIndex: 2}); + let cells = getNameCells(table); + assert.lengthOf(cells, 3); + assert.strictEqual('c', cells[0].row.name); + assert.strictEqual('b', cells[1].row.name); + assert.strictEqual('a', cells[2].row.name); + await table.viewState.update({sortDescending: true}); + cells = getNameCells(table); + assert.lengthOf(cells, 3); + assert.strictEqual('a', cells[0].row.name); + assert.strictEqual('b', cells[1].row.name); + assert.strictEqual('c', cells[2].row.name); + await table.viewState.update({sortColumnIndex: 1}); + cells = getNameCells(table); + assert.lengthOf(cells, 3); + assert.strictEqual('b', cells[0].row.name); + assert.strictEqual('a', cells[1].row.name); + assert.strictEqual('c', cells[2].row.name); + await table.viewState.update({sortDescending: false}); + cells = getNameCells(table); + assert.lengthOf(cells, 3); + assert.strictEqual('c', cells[0].row.name); + assert.strictEqual('a', cells[1].row.name); + assert.strictEqual('b', cells[2].row.name); + }); + + test('viewConstrainNameColumn', async function() { + // TODO(#4321): Switch to using skipped instead once it works + return; // https://github.com/catapult-project/catapult/issues/4320 + /* eslint-disable no-unreachable */ + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a'.repeat(100), tr.b.Unit.byName.count, []); + const table = await buildTable(this, histograms); + const nameCell = tr.b.getOnlyElement(getNameCells(table)); + assert.isTrue(nameCell.isOverflowing); + assert.isAbove(350, nameCell.getBoundingClientRect().width); + assert.isTrue(table.viewState.constrainNameColumn); + const dots = tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS); + assert.strictEqual('block', dots.style.display); + + await table.viewState.update({constrainNameColumn: false}); + assert.isFalse(nameCell.isOverflowing); + assert.isBelow(350, nameCell.getBoundingClientRect().width); + + await table.viewState.update({constrainNameColumn: true}); + assert.isTrue(nameCell.isOverflowing); + assert.isAbove(350, nameCell.getBoundingClientRect().width); + /* eslint-enable no-unreachable */ + }); + + test('controlConstrainNameColumn', async function() { + // TODO(#4321): Switch to using skipped instead once it works + return; // https://github.com/catapult-project/catapult/issues/4320 + /* eslint-disable no-unreachable */ + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a'.repeat(100), tr.b.Unit.byName.count, []); + const table = await buildTable(this, histograms); + const nameCell = tr.b.getOnlyElement(getNameCells(table)); + assert.isTrue(nameCell.isOverflowing); + assert.isAbove(350, nameCell.getBoundingClientRect().width); + assert.isTrue(table.viewState.constrainNameColumn); + const dots = tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS); + assert.strictEqual('block', dots.style.display); + + tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS).click(); + assert.isFalse(table.viewState.constrainNameColumn); + await tr.b.animationFrame(); + assert.isFalse(nameCell.isOverflowing); + assert.isBelow(350, nameCell.getBoundingClientRect().width); + + tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS).click(); + assert.isTrue(table.viewState.constrainNameColumn); + await tr.b.animationFrame(); + assert.isTrue(nameCell.isOverflowing); + assert.isAbove(350, nameCell.getBoundingClientRect().width); + /* eslint-enable no-unreachable */ + }); + + test('viewRowExpanded', async function() { + const histograms = new tr.v.HistogramSet(); + const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + aHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['A'])); + const bHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]); + bHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['B'])); + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + assert.lengthOf(getTableCells(table), 1); + + await table.viewState.tableRowStates.get('a').update({isExpanded: true}); + assert.lengthOf(getTableCells(table), 3); + + await table.viewState.tableRowStates.get('a').update({isExpanded: false}); + assert.lengthOf(getTableCells(table), 1); + }); + + test('controlRowExpanded', async function() { + const histograms = new tr.v.HistogramSet(); + const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + aHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['A'])); + const bHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]); + bHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['B'])); + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + assert.isFalse(table.viewState.tableRowStates.get('a').isExpanded); + + const nameCell = tr.b.getOnlyElement(getNameCells(table)); + nameCell.click(); + assert.isTrue(table.viewState.tableRowStates.get('a').isExpanded); + + nameCell.click(); + assert.isFalse(table.viewState.tableRowStates.get('a').isExpanded); + }); + + test('viewIsOverviewed', async function() { + const histograms = new tr.v.HistogramSet(); + let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])); + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + + const nameCells = getNameCells(table); + const cells = getTableCells(table); + assert.isFalse(nameCells[0].isOverviewed); + + await table.viewState.tableRowStates.get('a').update({isOverviewed: true}); + assert.isTrue(nameCells[0].isOverviewed); + + await table.viewState.tableRowStates.get('a').update({isOverviewed: false}); + assert.isFalse(nameCells[0].isOverviewed); + }); + + test('overviewSorted', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [4], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['D'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [2], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [3], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['C'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])], + ]), + }); + const table = await buildTable(this, histograms); + await table.viewState.update({ + groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ], + sortColumnIndex: 0, + sortDescending: true, + }); + await table.viewState.tableRowStates.get('a').update({isOverviewed: true}); + + let cells = getTableCells(table); + let chart = tr.ui.b.findDeepElementMatchingPredicate(cells[0], e => + e.tagName === 'svg' && e.parentNode.id === 'overview_container'); + assert.strictEqual('D', chart.data[0].x); + assert.strictEqual('C', chart.data[1].x); + assert.strictEqual('B', chart.data[2].x); + assert.strictEqual('A', chart.data[3].x); + + await table.viewState.update({ + sortDescending: false, + }); + cells = getTableCells(table); + chart = tr.ui.b.findDeepElementMatchingPredicate(cells[0], e => + e.tagName === 'svg' && e.parentNode.id === 'overview_container'); + assert.strictEqual('A', chart.data[0].x); + assert.strictEqual('B', chart.data[1].x); + assert.strictEqual('C', chart.data[2].x); + assert.strictEqual('D', chart.data[3].x); + }); + + test('controlIsOverviewed', async function() { + const histograms = new tr.v.HistogramSet(); + let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])); + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + + assert.isFalse(table.viewState.tableRowStates.get('a').isOverviewed); + + const nameCells = getNameCells(table); + tr.ui.b.findDeepElementMatchingPredicate(nameCells[0], e => + e.id === 'show_overview').click(); + assert.isTrue(table.viewState.tableRowStates.get('a').isOverviewed); + + tr.ui.b.findDeepElementMatchingPredicate(nameCells[0], e => + e.id === 'hide_overview').click(); + assert.isFalse(table.viewState.tableRowStates.get('a').isOverviewed); + }); + + test('overviewStatistic', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1, 1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1, 1, 1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + ]), + }); + const table = await buildTable(this, histograms); + await table.viewState.update({ + displayStatisticName: 'count', + groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ], + }); + await table.viewState.tableRowStates.get('a').update({isOverviewed: true}); + + let charts = tr.ui.b.findDeepElementsMatchingPredicate(table, e => + e.tagName === 'svg' && e.parentNode.id === 'overview_container'); + assert.lengthOf(charts, 3); + assert.lengthOf(charts[0].data, 2); + assert.lengthOf(charts[1].data, 2); + assert.lengthOf(charts[2].data, 2); + assert.strictEqual(charts[0].data[0].y, 3); + assert.strictEqual(charts[0].data[1].y, 7); + assert.strictEqual(charts[1].data[0].y, 1); + assert.strictEqual(charts[1].data[1].y, 2); + assert.strictEqual(charts[2].data[0].y, 3); + assert.strictEqual(charts[2].data[1].y, 4); + + await table.viewState.update({ + displayStatisticName: tr.v.DELTA + 'count', + referenceDisplayLabel: 'A', + }); + charts = tr.ui.b.findDeepElementsMatchingPredicate(table, e => + e.tagName === 'svg' && e.parentNode.id === 'overview_container'); + assert.lengthOf(charts, 3); + assert.lengthOf(charts[0].data, 2); + assert.lengthOf(charts[1].data, 2); + assert.lengthOf(charts[2].data, 2); + assert.strictEqual(charts[0].data[0].y, 3); + assert.strictEqual(charts[0].data[1].y, 7); + assert.strictEqual(charts[1].data[0].y, 1); + assert.strictEqual(charts[1].data[1].y, 2); + assert.strictEqual(charts[2].data[0].y, 2); + assert.strictEqual(charts[2].data[1].y, 2); + }); + + test('overviewUnits', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1, 1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + ]), + }); + histograms.createHistogram( + 'a', tr.b.Unit.byName.unitlessNumber, [1, 1, 1, 1], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + ]), + }); + const table = await buildTable(this, histograms); + await table.viewState.update({ + displayStatisticName: 'count', + groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ], + }); + await table.viewState.tableRowStates.get('a').update({ + isExpanded: true, + isOverviewed: true, + }); + await table.viewState.tableRowStates.get('a').subRows.get('X').update({ + isOverviewed: true, + }); + + const nameCells = getNameCells(table); + + // Check there is no overviewChart in name-cell when column units mismatch. + assert.isUndefined(tr.ui.b.findDeepElementMatching(nameCells[0], + '#overview_container svg')); + assert.isUndefined(tr.ui.b.findDeepElementMatching(nameCells[2], + '#overview_container svg')); + + // When column units match, the overview chart should exist and have the + // correct unit. + const nameOverviewChart = tr.ui.b.findDeepElementMatching(nameCells[1], + '#overview_container svg'); + assert.isDefined(nameOverviewChart); + assert.strictEqual(nameOverviewChart.unit, tr.b.Unit.byName.count); + + const cells = getTableCells(table); + + // When subrow units match, the overview chart should exist and have the + // correct unit. + const overviewChart = tr.ui.b.findDeepElementMatching(cells[0], + '#overview_container svg'); + assert.isDefined(overviewChart); + assert.strictEqual(overviewChart.unit, tr.b.Unit.byName.count); + + // Check there is no overviewChart in table-cell when subrow units mismatch. + assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[1], + '#overview_container svg')); + + // Check there is no overviewChart in table-cell when there are no subrows. + assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[2], + '#overview_container svg')); + assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[3], + '#overview_container svg')); + assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[4], + '#overview_container svg')); + assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[5], + '#overview_container svg')); + }); + + test('viewCellOpen', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + const table = await buildTable(this, histograms); + const cell = tr.b.getOnlyElement(getTableCells(table)); + assert.isFalse(cell.isHistogramOpen); + + await table.viewState.tableRowStates.get('a').cells.get('Value').update( + {isOpen: true}); + assert.isTrue(cell.isHistogramOpen); + + await table.viewState.tableRowStates.get('a').cells.get('Value').update( + {isOpen: false}); + assert.isFalse(cell.isHistogramOpen); + }); + + test('controlCellOpen', async function() { + const histograms = new tr.v.HistogramSet(); + let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])); + const table = await buildTable(this, histograms); + + assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('A') + .isOpen); + const cells = getTableCells(table); + + tr.ui.b.findDeepElementMatchingPredicate(cells[0], e => + e.tagName === 'TR-V-UI-SCALAR-SPAN').click(); + assert.isTrue(table.viewState.tableRowStates.get('a').cells.get('A') + .isOpen); + + tr.ui.b.findDeepElementMatchingPredicate(cells[0], e => + e.id === 'close_histogram').click(); + assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('A') + .isOpen); + + tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.id === 'open_histograms').click(); + assert.isTrue(table.viewState.tableRowStates.get('a').cells.get('A') + .isOpen); + assert.isTrue(table.viewState.tableRowStates.get('a').cells.get('B') + .isOpen); + + tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.id === 'close_histograms').click(); + assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('A') + .isOpen); + assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('B') + .isOpen); + }); + + test('rebin', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a', tr.b.Unit.byName.count, range(0, 100), { + binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + ]), + }); + histograms.createHistogram('a', tr.b.Unit.byName.count, range(50, 150), { + binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + ]), + }); + const table = await buildTable(this, histograms); + + const cells = getTableCells(table); + assert.lengthOf(cells, 2); + assert.lengthOf(cells[0].histogram.allBins, + 2 + tr.v.DEFAULT_REBINNED_COUNT); + assert.lengthOf(cells[1].histogram.allBins, + 2 + tr.v.DEFAULT_REBINNED_COUNT); + assert.strictEqual(cells[0].histogram.allBins[0].range.max, 0); + assert.strictEqual(cells[1].histogram.allBins[0].range.max, 0); + assert.strictEqual(cells[0].histogram.allBins[41].range.min, 200); + assert.strictEqual(cells[1].histogram.allBins[41].range.min, 200); + }); + + test('leafHistograms', async function() { + const histograms = new tr.v.HistogramSet(); + let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])); + const table = await buildTable(this, histograms); + assert.lengthOf(table.leafHistograms, 1); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + assert.lengthOf(table.leafHistograms, 2); + }); + + test('nameCellOverflow', async function() { + // TODO(#4321): Switch to using skipped instead once it works + return; // https://github.com/catapult-project/catapult/issues/4320 + /* eslint-disable no-unreachable */ + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('a'.repeat(100), tr.b.Unit.byName.count, []); + const table = await buildTable(this, histograms); + const nameCell = tr.b.getOnlyElement(getNameCells(table)); + assert.isTrue(nameCell.isOverflowing); + assert.isAbove(350, nameCell.getBoundingClientRect().width); + + const dots = tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS); + assert.strictEqual('block', dots.style.display); + dots.click(); + + await tr.b.animationFrame(); + assert.isFalse(nameCell.isOverflowing); + assert.isBelow(350, nameCell.getBoundingClientRect().width); + /* eslint-enable no-unreachable */ + }); + + test('nameCellOverflowOnExpand', async function() { + // TODO(#4321): Switch to using skipped instead once it works + return; // https://github.com/catapult-project/catapult/issues/4320 + /* eslint-disable no-unreachable */ + const histograms = new tr.v.HistogramSet(); + let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['0'.repeat(100)])); + hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []); + hist.diagnostics.set( + tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet(['1'.repeat(100)])); + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + + const dots = tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS); + assert.strictEqual('none', dots.style.display); + + const baseTable = getBaseTable(table); + await table.viewState.tableRowStates.get('a').update({isExpanded: true}); + + const nameCell = tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-NAME-CELL' && + e.row.name === '0'.repeat(100)); + await tr.b.animationFrame(); + assert.isTrue(nameCell.isOverflowing); + assert.isAbove(350, nameCell.getBoundingClientRect().width); + + assert.strictEqual('block', dots.style.display); + dots.click(); + + await tr.b.animationFrame(); + assert.isFalse(nameCell.isOverflowing); + assert.isBelow(350, nameCell.getBoundingClientRect().width); + /* eslint-enable no-unreachable */ + }); + + test('overviewCharts', async function() { + const binBoundaries = tr.v.HistogramBinBoundaries.createLinear(0, 150, 10); + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('foo', tr.b.Unit.byName.count, [0], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])], + ]), + }); + histograms.createHistogram('foo', tr.b.Unit.byName.count, [10], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])], + ]), + }); + histograms.createHistogram('foo', tr.b.Unit.byName.count, [100], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])], + ]), + }); + histograms.createHistogram('foo', tr.b.Unit.byName.count, [110], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])], + ]), + }); + histograms.createHistogram('bar', tr.b.Unit.byName.count, [0], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])], + ]), + }); + histograms.createHistogram('bar', tr.b.Unit.byName.count, [9], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])], + ]), + }); + histograms.createHistogram('bar', tr.b.Unit.byName.count, [90], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])], + ]), + }); + histograms.createHistogram('bar', tr.b.Unit.byName.count, [99], { + binBoundaries, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])], + ]), + }); + const now = new Date().getTime(); + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + ]}); + + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + table.viewState.tableRowStates.values())) { + await row.update({isOverviewed: true}); + } + + let charts = tr.ui.b.findDeepElementsMatchingPredicate( + table, e => ((e.id === 'overview_container') && + (e.style.display !== 'none'))); + charts = charts.map(div => div.children[0]); + assert.lengthOf(charts, 6); + + assert.deepEqual(JSON.stringify(charts[0].data), + JSON.stringify([{x: '0', y: 45}, {x: '1', y: 54}])); + tr.b.assertRangeEquals( + charts[0].dataRange, tr.b.math.Range.fromExplicitRange(0, 99)); + + assert.deepEqual( + charts[1].data, [{x: 'story0', y: 0}, {x: 'story1', y: 90}]); + tr.b.assertRangeEquals( + charts[1].dataRange, tr.b.math.Range.fromExplicitRange(0, 99)); + + assert.deepEqual( + charts[2].data, [{x: 'story0', y: 9}, {x: 'story1', y: 99}]); + tr.b.assertRangeEquals( + charts[2].dataRange, tr.b.math.Range.fromExplicitRange(0, 99)); + + assert.deepEqual(charts[3].data, [{x: '0', y: 50}, {x: '1', y: 60}]); + tr.b.assertRangeEquals( + charts[3].dataRange, tr.b.math.Range.fromExplicitRange(0, 110)); + + assert.deepEqual( + charts[4].data, [{x: 'story0', y: 0}, {x: 'story1', y: 100}]); + tr.b.assertRangeEquals( + charts[4].dataRange, tr.b.math.Range.fromExplicitRange(0, 110)); + + assert.deepEqual( + charts[5].data, [{x: 'story0', y: 10}, {x: 'story1', y: 110}]); + tr.b.assertRangeEquals( + charts[5].dataRange, tr.b.math.Range.fromExplicitRange(0, 110)); + + for (const row of tr.v.ui.HistogramSetTableRowState.walkAll( + table.viewState.tableRowStates.values())) { + await row.update({isOverviewed: false}); + } + + charts = tr.ui.b.findDeepElementsMatchingPredicate( + table, e => ((e.id === 'overview_container') && + (e.style.display !== 'none'))); + assert.lengthOf(charts, 0); + }); + + test('sortByDisplayStatistic', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram( + 'bar', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [0, 10], { + binBoundaries: TEST_BOUNDARIES, + }); + histograms.createHistogram( + 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [5], { + binBoundaries: TEST_BOUNDARIES, + }); + + const table = await buildTable(this, histograms); + await table.viewState.update({ + sortColumnIndex: 1, + sortDescending: false, + displayStatisticName: 'min', + }); + + let nameCells = getNameCells(table); + assert.strictEqual(nameCells[0].row.name, 'bar'); + assert.strictEqual(nameCells[1].row.name, 'foo'); + + await table.viewState.update({sortDescending: true}); + + nameCells = getNameCells(table); + assert.strictEqual(nameCells[0].row.name, 'foo'); + assert.strictEqual(nameCells[1].row.name, 'bar'); + + await table.viewState.update({displayStatisticName: 'max'}); + + nameCells = getNameCells(table); + assert.strictEqual(nameCells[0].row.name, 'bar'); + assert.strictEqual(nameCells[1].row.name, 'foo'); + + await table.viewState.update({sortDescending: false}); + + nameCells = getNameCells(table); + assert.strictEqual(nameCells[0].row.name, 'foo'); + assert.strictEqual(nameCells[1].row.name, 'bar'); + }); + + test('displayStatistic', async function() { + const histograms = new tr.v.HistogramSet(); + const now = new Date().getTime(); + const barHist = histograms.createHistogram( + 'a', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1, 2, 3], { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['bar'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)], + ]), + }); + const fooHist = histograms.createHistogram( + 'a', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [10, 20, 30], { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['foo'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)], + ]), + }); + + // Add a Histogram with another name so that the table displays the scalars. + const quxHist = histograms.createHistogram( + 'qux', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['foo'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)], + ]), + }); + + const table = await buildTable(this, histograms); + + function histogramsEqual(a, b) { + // This is not an exhaustive equality check. This only tests the fields + // that are distinguishing for this test(). + if (a.name !== b.name) return false; + return tr.v.HistogramGrouping.DISPLAY_LABEL.callback(a) === + tr.v.HistogramGrouping.DISPLAY_LABEL.callback(b); + } + + let fooCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + elem.histogram && + histogramsEqual(elem.histogram, fooHist))); + assert.isDefined(fooCell); + + let fooContent = tr.ui.b.findDeepElementMatchingPredicate( + fooCell, elem => elem.id === 'content'); + assert.isDefined(fooContent); + + let barCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + elem.histogram && + histogramsEqual(elem.histogram, barHist))); + assert.isDefined(barCell); + + let barContent = tr.ui.b.findDeepElementMatchingPredicate( + barCell, elem => elem.id === 'content'); + assert.isDefined(barContent); + + assert.strictEqual(table.viewState.displayStatisticName, 'avg'); + assert.strictEqual('20.000 ms', fooContent.textContent); + assert.strictEqual('2.000 ms', barContent.textContent); + + await table.viewState.update({referenceDisplayLabel: 'foo'}); + + fooCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + elem.histogram && + histogramsEqual(elem.histogram, fooHist))); + assert.isDefined(fooCell); + + fooContent = tr.ui.b.findDeepElementMatchingPredicate( + fooCell, elem => elem.id === 'content'); + assert.isDefined(fooContent); + + barCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + elem.histogram && + histogramsEqual(elem.histogram, barHist))); + assert.isDefined(barCell); + + barContent = tr.ui.b.findDeepElementMatchingPredicate( + barCell, elem => elem.id === 'content'); + assert.isDefined(barContent); + + await table.viewState.update({displayStatisticName: `${tr.v.DELTA}avg`}); + assert.strictEqual('20.000 ms', fooContent.textContent); + assert.strictEqual('-18.000 ms', barContent.textContent); + + await table.viewState.update({displayStatisticName: `%${tr.v.DELTA}avg`}); + + fooCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + elem.histogram && + histogramsEqual(elem.histogram, fooHist))); + assert.isDefined(fooCell); + + fooContent = tr.ui.b.findDeepElementMatchingPredicate( + fooCell, elem => elem.id === 'content'); + assert.isDefined(fooContent); + + barCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + elem.histogram && + histogramsEqual(elem.histogram, barHist))); + assert.isDefined(barCell); + + barContent = tr.ui.b.findDeepElementMatchingPredicate( + barCell, elem => elem.id === 'content'); + assert.isDefined(barContent); + + assert.strictEqual(table.viewState.displayStatisticName, + `%${tr.v.DELTA}avg`); + assert.strictEqual('20.000 ms', fooContent.textContent); + assert.strictEqual('-90.0%', barContent.textContent); + }); + + test('requestSelectionChange', async function() { + const histograms = new tr.v.HistogramSet(); + const barHist = histograms.createHistogram( + 'bar', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1], { + binBoundaries: TEST_BOUNDARIES, + }); + + const fooHist = histograms.createHistogram( + 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, { + value: 1, + diagnostics: { + breakdown: tr.v.d.Breakdown.fromEntries([ + ['bar', 1], + ['qux', 0], + ]), + }, + }, { + binBoundaries: TEST_BOUNDARIES, + }); + + const quxHist = histograms.createHistogram( + 'qux', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], { + binBoundaries: TEST_BOUNDARIES, + }); + const breakdown = new tr.v.d.RelatedNameMap(); + breakdown.set('bar', barHist.name); + breakdown.set('qux', quxHist.name); + fooHist.diagnostics.set('breakdown', breakdown); + + const table = await buildTable(this, histograms); + await table.viewState.update({showAll: false}); + + let fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isDefined(fooCell); + + let barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isUndefined(barCell); + + fooCell.isHistogramOpen = true; + + const barLink = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => elem.tagName === 'TR-UI-A-ANALYSIS-LINK'); + assert.isDefined(barLink); + barLink.click(); + + await tr.b.animationFrame(); + barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isDefined(barCell); + + await table.viewState.update({searchQuery: ''}); + + fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isDefined(fooCell); + + fooCell.isHistogramOpen = true; + + const selectAllLink = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-UI-A-ANALYSIS-LINK') && + (elem.textContent === 'Select All'))); + assert.isDefined(selectAllLink); + selectAllLink.click(); + + assert.strictEqual(table.viewState.searchQuery, '^(bar|qux)$'); + + await tr.b.animationFrame(); + fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isUndefined(fooCell); + + barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isDefined(barCell); + + const quxCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'qux'))); + assert.isDefined(quxCell); + }); + + test('search', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram( + 'bar', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1], { + binBoundaries: TEST_BOUNDARIES, + }); + histograms.createHistogram( + 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1], { + binBoundaries: TEST_BOUNDARIES, + }); + + const table = await buildTable(this, histograms); + + let fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isDefined(fooCell); + + let barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isDefined(barCell); + + await table.viewState.update({searchQuery: 'bar'}); + + fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isUndefined(fooCell); + + barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isDefined(barCell); + + await table.viewState.update({searchQuery: 'foo'}); + + fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isDefined(fooCell); + + barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isUndefined(barCell); + + // As users type in regexes, some intermediate forms may be invalid regexes. + // When the search is an invalid regex, just ignore it. + await table.viewState.update({searchQuery: '[a-'}); + + fooCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'foo'))); + assert.isDefined(fooCell); + + barCell = tr.ui.b.findDeepElementMatchingPredicate( + table, elem => ( + (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') && + (elem.histogram.name === 'bar'))); + assert.isDefined(barCell); + }); + + test('emptyAndMissing', async function() { + const now = new Date().getTime(); + const histograms = new tr.v.HistogramSet(); + + const histA = histograms.createHistogram( + 'histogram A', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + range(0, 100).map(i => Math.random() * 1e3), { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, + new tr.v.d.GenericSet(['iteration A'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)], + ]), + }); + + const histB = histograms.createHistogram( + 'histogram B', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + range(0, 100).map(i => Math.random() * 1e3), { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, + new tr.v.d.GenericSet(['iteration B'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)], + ]), + }); + + const histC = histograms.createHistogram( + 'histogram A', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, + new tr.v.d.GenericSet(['iteration B'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)], + ]), + }); + + const table = await buildTable(this, histograms); + + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.textContent === '(empty)')); + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate( + table, e => e.textContent === '(missing)')); + }); + + test('instantiate_1x1', async function() { + const histograms = new tr.v.HistogramSet(); + const hist = histograms.createHistogram( + 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + range(0, 100).map(i => Math.random() * 1e3), { + binBoundaries: TEST_BOUNDARIES, + }); + const table = await buildTable(this, histograms); + + const baseTable = getBaseTable(table); + assert.strictEqual(baseTable.tableRows.length, 1); + + const cell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => + elem.tagName === 'TR-V-UI-SCALAR-SPAN'); + cell.click(); + + const yAxisText = tr.ui.b.findDeepElementMatchingPredicate(table, e => + e.tagName === 'text' && e.textContent === '<0.000 ms'); + assert.isBelow(0, yAxisText.getBBox().width); + }); + + test('merge_unmergeable', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('foo', tr.b.Unit.byName.count, [], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['Value'])], + ]), + }); + histograms.createHistogram('foo', tr.b.Unit.byName.count, [], { + binBoundaries: tr.v.HistogramBinBoundaries.createLinear(0, 1e3, 21), + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])], + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['Value'])], + ]), + }); + const table = await buildTable(this, histograms); + assert.strictEqual(table.viewState.tableRowStates.size, 1); + assert.instanceOf(tr.b.getOnlyElement(getTableCells(table)).histogram, + tr.v.HistogramSet); + }); + + test('instantiate_1x5', async function() { + const histograms = new tr.v.HistogramSet(); + + for (let i = 0; i < 5; ++i) { + histograms.createHistogram( + 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + range(0, 100).map(i => Math.random() * 1e3), { + binBoundaries: TEST_BOUNDARIES, + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['' + i])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(new Date().getTime())], + ]), + }); + } + const table = await buildTable(this, histograms); + }); + + test('instantiate_2x2', async function() { + const histograms = new tr.v.HistogramSet(); + histograms.createHistogram('foo', tr.b.Unit.byName.count, + range(0, 100).map(i => Math.random() * 1e3), { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(new Date().getTime())], + ]), + }); + histograms.createHistogram('bar', tr.b.Unit.byName.count, + range(0, 100).map(i => Math.random() * 1e3), { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(new Date().getTime())], + ]), + }); + histograms.createHistogram('foo', tr.b.Unit.byName.count, + range(0, 100).map(i => Math.random() * 1e3), { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(new Date().getTime())], + ]), + }); + histograms.createHistogram('bar', tr.b.Unit.byName.count, + range(0, 100).map(i => Math.random() * 1e3), { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(new Date().getTime())], + ]), + }); + const table = await buildTable(this, histograms); + const baseTable = getBaseTable(table); + + assert.lengthOf(baseTable.tableColumns, 3); + assert.strictEqual('Name', + baseTable.tableColumns[0].title.children[0].textContent); + assert.strictEqual('A', + baseTable.tableColumns[1].title.textContent); + assert.strictEqual('B', + baseTable.tableColumns[2].title.textContent); + + await table.viewState.update({referenceDisplayLabel: 'A'}); + baseTable.rebuild(); + assert.strictEqual(1, baseTable.selectedTableColumnIndex); + let cells = getTableCells(table); + assert.strictEqual(cells[1].referenceHistogram, cells[0].histogram); + assert.strictEqual(cells[3].referenceHistogram, cells[2].histogram); + + await table.viewState.update({referenceDisplayLabel: 'B'}); + cells = getTableCells(table); + assert.strictEqual(2, baseTable.selectedTableColumnIndex); + assert.strictEqual(cells[0].referenceHistogram, cells[1].histogram); + assert.strictEqual(cells[2].referenceHistogram, cells[3].histogram); + + // Test sorting by the reference column when the displayStatistic is a delta + // statistic. + await table.viewState.update({sortColumnIndex: 2}); + let nameCell = getNameCells(table)[0]; + const originalFirstRow = nameCell.row.name; + // This is either 'foo' or 'bar' depending on Math.random() above. + + await table.viewState.update({ + sortDescending: !table.viewState.sortDescending, + }); + baseTable.rebuild(); + nameCell = getNameCells(table)[0]; + assert.notEqual(originalFirstRow, nameCell.row.name); + }); + + test('merged', async function() { + const histograms = new tr.v.HistogramSet(); + // Add 2^8=256 Histograms, all with the same name, with different metadata. + const benchmarkNames = ['bm A', 'bm B']; + const storyGroupingKeys0 = ['A', 'B']; + const storyGroupingKeys1 = ['C', 'D']; + const storyNames = ['story A', 'story B']; + const starts = [1439708400000, 1439794800000]; + const labels = ['label A', 'label B']; + const name = 'name '.repeat(20); + const unit = tr.b.Unit.byName.timeDurationInMs_smallerIsBetter; + + for (const benchmarkName of benchmarkNames) { + for (const storyGroupingKey0 of storyGroupingKeys0) { + for (const storyGroupingKey1 of storyGroupingKeys1) { + for (const storyName of storyNames) { + for (const startMs of starts) { + for (let storysetCounter = 0; storysetCounter < 2; + ++storysetCounter) { + for (const label of labels) { + const samples = range(0, 100).map(i => { + return { + value: Math.random() * 1e3, + diagnostics: {i: new tr.v.d.GenericSet([i])}, + }; + }); + histograms.createHistogram(name, unit, samples, { + description: 'The best description.', + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.LABELS, + new tr.v.d.GenericSet([label])], + [tr.v.d.RESERVED_NAMES.STORYSET_REPEATS, + new tr.v.d.GenericSet([storysetCounter])], + [tr.v.d.RESERVED_NAMES.BENCHMARKS, + new tr.v.d.GenericSet([benchmarkName])], + [tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(startMs)], + [tr.v.d.RESERVED_NAMES.STORIES, + new tr.v.d.GenericSet([storyName])], + [tr.v.d.RESERVED_NAMES.STORY_TAGS, + new tr.v.d.GenericSet([ + `storyGroupingKey0:${storyGroupingKey0}`, + `storyGroupingKey1:${storyGroupingKey1}`, + ])], + ]), + }); + } + } + } + } + } + } + } + histograms.buildGroupingsFromTags([tr.v.d.RESERVED_NAMES.STORY_TAGS]); + + const table = await buildTable(this, histograms); + await table.viewState.update({groupings: [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.BENCHMARKS), + tr.v.HistogramGrouping.BY_KEY.get('storyGroupingKey0Tag'), + tr.v.HistogramGrouping.BY_KEY.get('storyGroupingKey1Tag'), + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES), + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.BENCHMARK_START), + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORYSET_REPEATS), + ]}); + const baseTable = getBaseTable(table); + + assert.lengthOf(baseTable.tableColumns, 3); + const nameHeaderCell = baseTable.tableColumns[0].title; + assert.strictEqual('Name', nameHeaderCell.children[0].textContent); + assert.strictEqual('label A', baseTable.tableColumns[1].title.textContent); + assert.strictEqual('label B', baseTable.tableColumns[2].title.textContent); + + const nameCell = tr.b.getOnlyElement(getNameCells(table)); + assert.closeTo(346, nameCell.getBoundingClientRect().width, 1); + + nameHeaderCell.children[1].click(); + // toggleNameColumnWidth_ does not await viewState.update() + await tr.b.animationFrame(); + assert.isBelow(322, nameCell.getBoundingClientRect().width); + + nameHeaderCell.children[1].click(); + await tr.b.animationFrame(); + assert.closeTo(346, nameCell.getBoundingClientRect().width, 1); + + assert.lengthOf(baseTable.tableRows, 1); + assert.strictEqual(name, baseTable.tableRows[0].name); + assert.lengthOf(baseTable.tableRows[0].subRows, 2); + + // assertions only report their arguments, which is not enough information + // to diagnose problems with nested structures like tableRows -- the path to + // the particular row is needed. This code would be a bit simpler if each + // row were given a named variable, but the path to each subRow would still + // need to be tracked in order to provide for diagnosing. + const subRowPath = []; + function getSubRow() { + let row = baseTable.tableRows[0]; + for (const index of subRowPath) { + row = row.subRows[index]; + } + return row; + } + + for (let i = 0; i < benchmarkNames.length; ++i) { + subRowPath.push(i); + assert.lengthOf(getSubRow().subRows, 2, subRowPath); + assert.strictEqual(benchmarkNames[i], getSubRow().name, subRowPath); + + for (let s = 0; s < storyGroupingKeys0.length; ++s) { + subRowPath.push(s); + assert.lengthOf(getSubRow().subRows, 2, subRowPath); + assert.strictEqual(storyGroupingKeys0[s], getSubRow().name, subRowPath); + + for (let t = 0; t < storyGroupingKeys1.length; ++t) { + subRowPath.push(t); + assert.lengthOf(getSubRow().subRows, 2, subRowPath); + assert.strictEqual(storyGroupingKeys1[t], getSubRow().name, + subRowPath); + + for (let j = 0; j < storyNames.length; ++j) { + subRowPath.push(j); + assert.lengthOf(getSubRow().subRows, 2, subRowPath); + assert.strictEqual(storyNames[j], getSubRow().name, subRowPath); + + for (let k = 0; k < starts.length; ++k) { + subRowPath.push(k); + assert.lengthOf(getSubRow().subRows, 2, subRowPath); + assert.strictEqual(tr.b.formatDate(new Date(starts[k])), + getSubRow().name, subRowPath); + + for (let l = 0; l < 2; ++l) { + subRowPath.push(l); + assert.lengthOf(getSubRow().subRows, 0, subRowPath); + assert.strictEqual('' + l, getSubRow().name, subRowPath); + subRowPath.pop(); + } + subRowPath.pop(); + } + subRowPath.pop(); + } + subRowPath.pop(); + } + subRowPath.pop(); + } + subRowPath.pop(); + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html new file mode 100644 index 00000000000..18ba824811f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html @@ -0,0 +1,210 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/timing.html"> +<link rel="import" href="/tracing/ui/null_brushing_state_controller.html"> +<link rel="import" href="/tracing/value/csv_builder.html"> +<link rel="import" href="/tracing/value/histogram_parameter_collector.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_controls.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_table.html"> +<link rel="import" href="/tracing/value/ui/visualizations_data_container.html"> + +<dom-module id="tr-v-ui-histogram-set-view"> + <template> + <style> + :host { + font-family: sans-serif; + } + + #zero { + color: red; + /* histogram-set-table is used by both metrics-side-panel and results.html. + * This font-size rule has no effect in results.html, but improves + * legibility in the metrics-side-panel, which sets font-size in order to + * make this table denser. + */ + font-size: initial; + } + + #container { + display: none; + } + + #visualizations{ + display: none; + } + </style> + + <div id="zero">zero Histograms</div> + + <div id="container"> + <tr-v-ui-histogram-set-controls id="controls"> + </tr-v-ui-histogram-set-controls> + + <tr-v-ui-visualizations-data-container id="visualizations"> + </tr-v-ui-visualizations-data-container> + + <tr-v-ui-histogram-set-table id="table"></tr-v-ui-histogram-set-table> + </div> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-histogram-set-view', + + listeners: { + export: 'onExport_', + loadVisualization: 'onLoadVisualization_' + }, + + created() { + this.brushingStateController_ = new tr.ui.NullBrushingStateController(); + this.viewState_ = new tr.v.ui.HistogramSetViewState(); + this.visualizationLoaded_ = false; + }, + + ready() { + this.$.table.viewState = this.viewState; + this.$.controls.viewState = this.viewState; + }, + + attached() { + this.brushingStateController.parentController = + tr.c.BrushingStateController.getControllerForElement(this.parentNode); + }, + + get brushingStateController() { + return this.brushingStateController_; + }, + + get viewState() { + return this.viewState_; + }, + + get histograms() { + return this.$.table.histograms; + }, + + /** + * @param {!tr.v.HistogramSet} histograms + * @param {!Object=} opt_options + * @param {function(string):!Promise=} opt_options.progressconst + * @param {string=} opt_options.helpHref + * @param {string=} opt_options.feedbackHref + */ + async build(histograms, opt_options) { + const options = opt_options || {}; + const progress = options.progress || (() => Promise.resolve()); + + if (options.helpHref) this.$.controls.helpHref = options.helpHref; + if (options.feedbackHref) { + this.$.controls.feedbackHref = options.feedbackHref; + } + + if (histograms === undefined || histograms.length === 0) { + this.$.container.style.display = 'none'; + this.$.zero.style.display = 'block'; + this.style.display = 'block'; + return; + } + this.$.zero.style.display = 'none'; + this.$.container.style.display = 'block'; + this.$.container.style.maxHeight = (window.innerHeight - 16) + 'px'; + + const buildMark = tr.b.Timing.mark('histogram-set-view', 'build'); + await progress('Finding important Histograms...'); + const sourceHistogramsMark = tr.b.Timing.mark( + 'histogram-set-view', 'sourceHistograms'); + const sourceHistograms = histograms.sourceHistograms; + sourceHistogramsMark.end(); + // Disable show_all if all values are sourceHistograms. + this.$.controls.showAllEnabled = ( + sourceHistograms.length !== histograms.length); + + await progress('Collecting parameters...'); + const collectParametersMark = tr.b.Timing.mark( + 'histogram-set-view', 'collectParameters'); + const parameterCollector = new tr.v.HistogramParameterCollector(); + parameterCollector.process(histograms); + this.$.controls.baseStatisticNames = parameterCollector.statisticNames; + this.$.controls.possibleGroupings = parameterCollector.possibleGroupings; + const displayLabels = parameterCollector.labels; + this.$.controls.displayLabels = displayLabels; + collectParametersMark.end(); + + // Only show visualizer button if values are from rendering benchmarks + const hist = [...histograms][0]; + const benchmarks = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.BENCHMARKS); + let enable = false; + if (benchmarks !== undefined && benchmarks.length > 0) { + for (const benchmark of benchmarks) { + if (benchmark.includes('rendering')) { + enable = true; + break; + } + } + } + this.$.controls.enableVisualization = enable; + + // Table.build() displays its own progress. + await this.$.table.build( + histograms, sourceHistograms, displayLabels, progress); + + buildMark.end(); + }, + + onExport_(event) { + const mark = tr.b.Timing.mark('histogram-set-view', 'export' + + (event.merged ? 'Merged' : 'Raw') + event.format.toUpperCase()); + + const histograms = event.merged ? this.$.table.leafHistograms : + this.histograms; + + let blob; + if (event.format === 'csv') { + const csv = new tr.v.CSVBuilder(histograms); + csv.build(); + blob = new window.Blob([csv.toString()], {type: 'text/csv'}); + } else if (event.format === 'json') { + blob = new window.Blob([JSON.stringify(histograms.asDicts())], + {type: 'text/json'}); + } else { + throw new Error(`Unable to export format "${event.format}"`); + } + + const path = window.location.pathname.split('/'); + const basename = path[path.length - 1].split('.')[0] || 'histograms'; + + const anchor = document.createElement('a'); + anchor.download = `${basename}.${event.format}`; + anchor.href = window.URL.createObjectURL(blob); + anchor.click(); + mark.end(); + }, + + onLoadVisualization_(event) { + if (!this.visualizationLoaded_) { // Initial loading + this.$.visualizations.style.display = 'block'; + this.$.visualizations.build(this.$.table.leafHistograms, + this.histograms); + this.visualizationLoaded_ = true; + } else if (this.$.visualizations.style.display === 'none') { + // Toggle to hide + this.$.visualizations.style.display = 'block'; + } else { + this.$.visualizations.style.display = 'none'; + } + }, + }); + + return { + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html new file mode 100644 index 00000000000..ade4ef2a9c4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html @@ -0,0 +1,144 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/view_state.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + class HistogramSetViewState extends tr.b.ViewState { + constructor() { + super(); + this.define('searchQuery', ''); + this.define('referenceDisplayLabel', ''); + this.define('displayStatisticName', ''); + this.define('showAll', true); + this.define('groupings', []); + this.define('sortColumnIndex', 0); + this.define('sortDescending', false); + this.define('constrainNameColumn', true); + this.define('tableRowStates', new Map()); + this.define('alpha', 0.01); + } + } + + tr.b.ViewState.register(HistogramSetViewState); + + class HistogramSetTableRowState extends tr.b.ViewState { + constructor() { + super(); + this.define('isExpanded', false); + this.define('isOverviewed', false); + this.define('cells', new Map()); + this.define('subRows', new Map()); + this.define('diagnosticsTab', ''); + } + + asCompactDict() { + const result = {}; + if (this.isExpanded) result.e = '1'; + if (this.isOverviewed) result.o = '1'; + if (this.diagnosticsTab) result.d = this.diagnosticsTab; + const cells = {}; + for (const [name, cell] of this.cells) { + const cellDict = cell.asCompactDict(); + if (cellDict === undefined) continue; + cells[name] = cellDict; + } + if (Object.keys(cells).length > 0) result.c = cells; + + const subRows = {}; + for (const [name, row] of this.subRows) { + const rowDict = row.asCompactDict(); + if (rowDict === undefined) continue; + subRows[name] = rowDict; + } + if (Object.keys(subRows).length > 0) result.r = subRows; + + if (Object.keys(result).length === 0) return undefined; + + return result; + } + + async updateFromCompactDict(dict) { + await this.update({ + isExpanded: dict.e === '1', + isOverviewed: dict.o === '1', + diagnosticsTab: dict.d || '', + }); + + for (const [name, cellDict] of Object.entries(dict.c || {})) { + const cell = this.cells.get(name); + if (cell === undefined) continue; + await cell.updateFromCompactDict(cellDict); + } + + for (const [name, subRowDict] of Object.entries(dict.r || {})) { + const subRow = this.subRows.get(name); + if (subRow === undefined) continue; + await subRow.updateFromCompactDict(subRowDict); + } + } + + * walk() { + yield this; + for (const row of this.subRows.values()) yield* row.walk(); + } + + static* walkAll(rootRows) { + for (const rootRow of rootRows) yield* rootRow.walk(); + } + } + + tr.b.ViewState.register(HistogramSetTableRowState); + + class HistogramSetTableCellState extends tr.b.ViewState { + constructor() { + super(); + this.define('isOpen', false); + this.define('brushedBinRange', new tr.b.math.Range()); + this.define('mergeSampleDiagnostics', true); + } + + asCompactDict() { + const result = {}; + if (this.isOpen) result.o = '1'; + if (!this.mergeSampleDiagnostics) result.m = '0'; + if (!this.brushedBinRange.isEmpty) { + result.b = this.brushedBinRange.min + '_' + this.brushedBinRange.max; + } + if (Object.keys(result).length === 0) return undefined; + return result; + } + + async updateFromCompactDict(dict) { + let binRange = this.brushedBinRange; + if (dict.b) { + let [bMin, bMax] = dict.b.split('_'); + bMin = parseInt(bMin); + bMax = parseInt(bMax); + if (bMin !== binRange.min || bMax !== binRange.max) { + binRange = tr.b.math.Range.fromExplicitRange(bMin, bMax); + } + } + await this.update({ + isOpen: dict.o === '1', + brushedBinRange: binRange, + mergeSampleDiagnostics: dict.m !== '0', + }); + } + } + + tr.b.ViewState.register(HistogramSetTableCellState); + + return { + HistogramSetTableCellState, + HistogramSetTableRowState, + HistogramSetViewState, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html new file mode 100644 index 00000000000..ede486c98d5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate0', async function() { + const view = document.createElement('tr-v-ui-histogram-set-view'); + const histograms = new tr.v.HistogramSet(); + + const hist = new tr.v.Histogram('a', tr.b.Unit.byName.normalizedPercentage); + for (let i = 0; i < 1e2; ++i) { + hist.addSample(Math.random()); + } + histograms.addHistogram(hist); + + this.addHTMLOutput(view); + await view.build(histograms); + + assert.strictEqual('none', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.textContent === 'zero Histograms')).display); + assert.strictEqual('block', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.id === 'container')).display); + }); + + test('implicitUndefinedHistogramSet', async function() { + const view = document.createElement('tr-v-ui-histogram-set-view'); + this.addHTMLOutput(view); + assert.strictEqual('block', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.textContent === 'zero Histograms')).display); + assert.strictEqual('none', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.id === 'container')).display); + }); + + test('explicitUndefinedHistogramSet', async function() { + const view = document.createElement('tr-v-ui-histogram-set-view'); + this.addHTMLOutput(view); + view.build(undefined); + assert.strictEqual('block', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.textContent === 'zero Histograms')).display); + assert.strictEqual('none', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.id === 'container')).display); + }); + + test('emptyHistogramSet', async function() { + const view = document.createElement('tr-v-ui-histogram-set-view'); + this.addHTMLOutput(view); + view.build(new tr.v.HistogramSet()); + assert.strictEqual('block', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.textContent === 'zero Histograms')).display); + assert.strictEqual('none', getComputedStyle( + tr.ui.b.findDeepElementMatchingPredicate( + view, e => e.id === 'container')).display); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html new file mode 100644 index 00000000000..c0382f66b93 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html @@ -0,0 +1,599 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/base/timing.html"> +<link rel="import" href="/tracing/ui/base/box_chart.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/base/name_bar_chart.html"> +<link rel="import" href="/tracing/ui/base/tab_view.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_map_table.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> +<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html"> +<link rel="import" href="/tracing/value/ui/scalar_map_table.html"> + +<dom-module id="tr-v-ui-histogram-span"> + <template> + <style> + #container { + display: flex; + flex-direction: row; + justify-content: space-between; + } + #chart { + flex-grow: 1; + display: none; + } + #drag_handle, #diagnostics_tab_templates { + display: none; + } + #chart svg { + display: block; + } + #stats_container { + overflow-y: auto; + } + </style> + + <div id="container"> + <div id="chart"></div> + <div id="stats_container"> + <tr-v-ui-scalar-map-table id="stats"></tr-v-ui-scalar-map-table> + </div> + </div> + <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle> + + <tr-ui-b-tab-view id="diagnostics"></tr-ui-b-tab-view> + + <div id="diagnostics_tab_templates"> + <tr-v-ui-diagnostic-map-table id="metric_diagnostics"></tr-v-ui-diagnostic-map-table> + + <tr-v-ui-diagnostic-map-table id="metadata_diagnostics"></tr-v-ui-diagnostic-map-table> + + <div id="sample_diagnostics_container"> + <div id="merge_sample_diagnostics_container"> + <input type="checkbox" id="merge_sample_diagnostics" checked on-change="updateDiagnostics_"> + <label for="merge_sample_diagnostics">Merge Sample Diagnostics</label> + </div> + <tr-v-ui-diagnostic-map-table id="sample_diagnostics"></tr-v-ui-diagnostic-map-table> + </div> + </div> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + const DEFAULT_BAR_HEIGHT_PX = 5; + const TRUNCATE_BIN_MARGIN = 0.15; + const IGNORE_DELTA_STATISTICS_NAMES = [ + `${tr.v.DELTA}min`, + `%${tr.v.DELTA}min`, + `${tr.v.DELTA}max`, + `%${tr.v.DELTA}max`, + `${tr.v.DELTA}sum`, + `%${tr.v.DELTA}sum`, + `${tr.v.DELTA}count`, + `%${tr.v.DELTA}count`, + ]; + + Polymer({ + is: 'tr-v-ui-histogram-span', + + created() { + this.viewStateListener_ = this.onViewStateUpdate_.bind(this); + this.viewState = new tr.v.ui.HistogramSetTableCellState(); + this.rowStateListener_ = this.onRowStateUpdate_.bind(this); + this.rowState = new tr.v.ui.HistogramSetTableRowState(); + this.rootStateListener_ = this.onRootStateUpdate_.bind(this); + this.rootState = new tr.v.ui.HistogramSetViewState(); + + this.histogram_ = undefined; + this.referenceHistogram_ = undefined; + this.graphWidth_ = undefined; + this.graphHeight_ = undefined; + this.mouseDownBin_ = undefined; + this.prevBrushedBinRange_ = new tr.b.math.Range(); + this.anySampleDiagnostics_ = false; + this.canMergeSampleDiagnostics_ = true; + this.mwuResult_ = undefined; + }, + + get rowState() { + return this.rowState_; + }, + + set rowState(rs) { + if (this.rowState) { + this.rowState.removeUpdateListener(this.rowStateListener_); + } + this.rowState_ = rs; + this.rowState.addUpdateListener(this.rowStateListener_); + if (this.isAttached) this.updateContents_(); + }, + + get viewState() { + return this.viewState_; + }, + + set viewState(vs) { + if (this.viewState) { + this.viewState.removeUpdateListener(this.viewStateListener_); + } + this.viewState_ = vs; + this.viewState.addUpdateListener(this.viewStateListener_); + if (this.isAttached) this.updateContents_(); + }, + + get rootState() { + return this.rootState_; + }, + + set rootState(vs) { + if (this.rootState) { + this.rootState.removeUpdateListener(this.rootStateListener_); + } + this.rootState_ = vs; + this.rootState.addUpdateListener(this.rootStateListener_); + if (this.isAttached) this.updateContents_(); + }, + + build(histogram, opt_referenceHistogram) { + this.histogram_ = histogram; + this.$.metric_diagnostics.histogram = histogram; + this.$.sample_diagnostics.histogram = histogram; + this.referenceHistogram_ = opt_referenceHistogram; + + if (this.histogram.canCompare(this.referenceHistogram)) { + this.mwuResult_ = tr.b.math.Statistics.mwu( + this.histogram.sampleValues, + this.referenceHistogram.sampleValues, + this.rootState.alpha); + } + + this.anySampleDiagnostics_ = false; + for (const bin of this.histogram.allBins) { + if (bin.diagnosticMaps.length > 0) { + this.anySampleDiagnostics_ = true; + break; + } + } + + if (this.isAttached) this.updateContents_(); + }, + + onViewStateUpdate_(event) { + if (event.delta.brushedBinRange) { + if (this.chart_ !== undefined) { + this.chart_.brushedRange = this.viewState.brushedBinRange; + } + this.updateDiagnostics_(); + } + + if (event.delta.mergeSampleDiagnostics && + (this.viewState.mergeSampleDiagnostics !== + this.$.merge_sample_diagnostics.checked)) { + this.$.merge_sample_diagnostics.checked = + this.canMergeSampleDiagnostics && + this.viewState.mergeSampleDiagnostics; + this.updateDiagnostics_(); + } + }, + + updateSignificance_() { + if (!this.mwuResult_) return; + this.$.stats.setSignificanceForKey( + `${tr.v.DELTA}avg`, this.mwuResult_.significance); + }, + + onRootStateUpdate_(event) { + if (event.delta.alpha && this.mwuResult_) { + this.mwuResult_.compare(this.rootState.alpha); + this.updateSignificance_(); + } + }, + + onRowStateUpdate_(event) { + if (event.delta.diagnosticsTab) { + if (this.rowState.diagnosticsTab === + this.$.sample_diagnostics_container.tabLabel) { + this.updateDiagnostics_(); + } else { + for (const tab of this.$.diagnostics.subViews) { + if (this.rowState.diagnosticsTab === tab.tabLabel) { + this.$.diagnostics.selectedSubView = tab; + break; + } + } + } + } + }, + + ready() { + this.$.metric_diagnostics.tabLabel = 'histogram diagnostics'; + this.$.sample_diagnostics_container.tabLabel = 'sample diagnostics'; + this.$.metadata_diagnostics.tabLabel = 'metadata'; + this.$.metadata_diagnostics.isMetadata = true; + this.$.diagnostics.addEventListener( + 'selected-tab-change', this.onSelectedDiagnosticsChanged_.bind(this)); + this.$.drag_handle.target = this.$.container; + this.$.drag_handle.addEventListener( + 'drag-handle-resize', this.onResize_.bind(this)); + }, + + attached() { + if (this.histogram_ !== undefined) this.updateContents_(); + }, + + get canMergeSampleDiagnostics() { + return this.canMergeSampleDiagnostics_; + }, + + set canMergeSampleDiagnostics(merge) { + this.canMergeSampleDiagnostics_ = merge; + if (!merge) this.viewState.mergeSampleDiagnostics = false; + this.$.merge_sample_diagnostics_container.style.display = ( + merge ? '' : 'none'); + }, + + onResize_(event) { + event.stopPropagation(); + let heightPx = parseInt(this.$.container.style.height); + if (heightPx < this.defaultGraphHeight) { + heightPx = this.defaultGraphHeight; + this.$.container.style.height = this.defaultGraphHeight + 'px'; + } + this.chart_.graphHeight = heightPx - (this.chart_.margin.top + + this.chart_.margin.bottom); + this.$.stats_container.style.maxHeight = + this.chart_.getBoundingClientRect().height + 'px'; + }, + + /** + * Get the width in pixels of the widest bar in the bar chart, not the total + * bar chart svg tag, which includes margins containing axes and legend. + * + * @return {number} + */ + get graphWidth() { + return this.graphWidth_ || this.defaultGraphWidth; + }, + + /** + * Set the width in pixels of the widest bar in the bar chart, not the total + * bar chart svg tag, which includes margins containing axes and legend. + * + * @param {number} width + */ + set graphWidth(width) { + this.graphWidth_ = width; + }, + + /** + * Get the height in pixels of the bars in the bar chart, not the total + * bar chart svg tag, which includes margins containing axes and legend. + * + * @return {number} + */ + get graphHeight() { + return this.graphHeight_ || this.defaultGraphHeight; + }, + + /** + * Set the height in pixels of the bars in the bar chart, not the total + * bar chart svg tag, which includes margins containing axes and legend. + * + * @param {number} height + */ + set graphHeight(height) { + this.graphHeight_ = height; + }, + + /** + * Get the height in pixels of one bar in the bar chart. + * + * @return {number} + */ + get barHeight() { + return this.chart_.barHeight; + }, + + /** + * Set the height in pixels of one bar in the bar chart. + * + * @param {number} px + */ + set barHeight(px) { + this.graphHeight = this.computeChartHeight_(px); + }, + + computeChartHeight_(barHeightPx) { + return (this.chart_.margin.top + + this.chart_.margin.bottom + + (barHeightPx * this.histogram.allBins.length)); + }, + + get defaultGraphHeight() { + if (this.histogram && this.histogram.allBins.length === 1) { + return 150; + } + return this.computeChartHeight_(DEFAULT_BAR_HEIGHT_PX); + }, + + get defaultGraphWidth() { + if (this.histogram.allBins.length === 1) { + return 100; + } + return 300; + }, + + get brushedBins() { + const bins = []; + if (this.histogram && !this.viewState.brushedBinRange.isEmpty) { + for (let i = this.viewState.brushedBinRange.min; + i < this.viewState.brushedBinRange.max; ++i) { + bins.push(this.histogram.allBins[i]); + } + } + return bins; + }, + + async updateBrushedRange_(binIndex) { + const brushedBinRange = new tr.b.math.Range(); + brushedBinRange.addValue(tr.b.math.clamp( + this.mouseDownBinIndex_, 0, this.histogram.allBins.length - 1)); + brushedBinRange.addValue(tr.b.math.clamp( + binIndex, 0, this.histogram.allBins.length - 1)); + brushedBinRange.max += 1; + await this.viewState.update({brushedBinRange}); + }, + + onMouseDown_(chartEvent) { + chartEvent.stopPropagation(); + if (!this.histogram) return; + this.prevBrushedBinRange_ = this.viewState.brushedBinRange; + this.mouseDownBinIndex_ = chartEvent.y; + this.updateBrushedRange_(chartEvent.y); + }, + + onMouseMove_(chartEvent) { + chartEvent.stopPropagation(); + if (!this.histogram) return; + this.updateBrushedRange_(chartEvent.y); + }, + + onMouseUp_(chartEvent) { + chartEvent.stopPropagation(); + if (!this.histogram) return; + this.updateBrushedRange_(chartEvent.y); + if (this.prevBrushedBinRange_.range === 1 && + this.viewState.brushedBinRange.range === 1 && + (this.prevBrushedBinRange_.min === + this.viewState.brushedBinRange.min)) { + tr.b.Timing.instant('histogram-span', 'clearBrushedBins'); + this.viewState.update({brushedBinRange: new tr.b.math.Range()}); + } else { + tr.b.Timing.instant('histogram-span', 'brushBins'); + } + this.mouseDownBinIndex_ = undefined; + }, + + async onSelectedDiagnosticsChanged_() { + await this.rowState.update({ + diagnosticsTab: this.$.diagnostics.selectedSubView.tabLabel, + }); + if ((this.$.diagnostics.selectedSubView === + this.$.sample_diagnostics_container) && + this.histogram && + this.viewState.brushedBinRange.isEmpty) { + // When the user selects the sample diagnostics tab, if they haven't + // already brushed any bins, then automatically brush all bins. + const brushedBinRange = tr.b.math.Range.fromExplicitRange( + 0, this.histogram.allBins.length); + await this.viewState.update({brushedBinRange}); + this.updateDiagnostics_(); + } + }, + + updateDiagnostics_() { + let maps = []; + for (const bin of this.brushedBins) { + for (const map of bin.diagnosticMaps) { + maps.push(map); + } + } + + if (this.$.merge_sample_diagnostics.checked !== + this.viewState.mergeSampleDiagnostics) { + this.viewState.update({ + mergeSampleDiagnostics: this.$.merge_sample_diagnostics.checked}); + } + + if (this.viewState.mergeSampleDiagnostics) { + const merged = new tr.v.d.DiagnosticMap(); + for (const map of maps) { + merged.addDiagnostics(map); + } + maps = [merged]; + } + + const mark = tr.b.Timing.mark('histogram-span', + (this.viewState.mergeSampleDiagnostics ? 'merge' : 'split') + + 'SampleDiagnostics'); + this.$.sample_diagnostics.diagnosticMaps = maps; + mark.end(); + + if (this.anySampleDiagnostics_) { + this.$.diagnostics.selectedSubView = + this.$.sample_diagnostics_container; + } + }, + + get histogram() { + return this.histogram_; + }, + + get referenceHistogram() { + return this.referenceHistogram_; + }, + + getDeltaScalars_(statNames, scalarMap) { + if (!this.histogram.canCompare(this.referenceHistogram)) return; + + for (const deltaStatName of tr.v.Histogram.getDeltaStatisticsNames( + statNames)) { + if (IGNORE_DELTA_STATISTICS_NAMES.includes(deltaStatName)) continue; + const scalar = this.histogram.getStatisticScalar( + deltaStatName, this.referenceHistogram, this.mwuResult_); + if (scalar === undefined) continue; + scalarMap.set(deltaStatName, scalar); + } + }, + + set isYLogScale(logScale) { + this.chart_.isYLogScale = logScale; + }, + + async updateContents_() { + this.$.chart.style.display = 'none'; + this.$.drag_handle.style.display = 'none'; + this.$.container.style.justifyContent = ''; + + while (Polymer.dom(this.$.chart).lastChild) { + Polymer.dom(this.$.chart).removeChild( + Polymer.dom(this.$.chart).lastChild); + } + + if (!this.histogram) return; + this.$.container.style.display = ''; + + const scalarMap = new Map(); + this.getDeltaScalars_(this.histogram.statisticsNames, scalarMap); + for (const [name, scalar] of this.histogram.statisticsScalars) { + scalarMap.set(name, scalar); + } + this.$.stats.scalarMap = scalarMap; + this.updateSignificance_(); + + const metricDiagnosticMap = new tr.v.d.DiagnosticMap(); + const metadataDiagnosticMap = new tr.v.d.DiagnosticMap(); + for (const [key, diagnostic] of this.histogram.diagnostics) { + // Hide implementation details. + if (diagnostic instanceof tr.v.d.RelatedNameMap) continue; + + if (tr.v.d.RESERVED_NAMES_SET.has(key)) { + metadataDiagnosticMap.set(key, diagnostic); + } else { + metricDiagnosticMap.set(key, diagnostic); + } + } + + const diagnosticTabs = []; + if (metricDiagnosticMap.size) { + this.$.metric_diagnostics.diagnosticMaps = [metricDiagnosticMap]; + diagnosticTabs.push(this.$.metric_diagnostics); + } + if (this.anySampleDiagnostics_) { + diagnosticTabs.push(this.$.sample_diagnostics_container); + } + if (metadataDiagnosticMap.size) { + this.$.metadata_diagnostics.diagnosticMaps = [metadataDiagnosticMap]; + diagnosticTabs.push(this.$.metadata_diagnostics); + } + this.$.diagnostics.resetSubViews(diagnosticTabs); + this.$.diagnostics.set('tabsHidden', diagnosticTabs.length < 2); + + if (this.histogram.numValues <= 1) { + await this.viewState.update({ + brushedBinRange: tr.b.math.Range.fromExplicitRange( + 0, this.histogram.allBins.length)}); + this.$.container.style.justifyContent = 'flex-end'; + return; + } + + this.$.chart.style.display = 'block'; + this.$.drag_handle.style.display = 'block'; + + if (this.histogram.allBins.length === 1) { + if (this.histogram.min !== this.histogram.max) { + this.chart_ = new tr.ui.b.BoxChart(); + Polymer.dom(this.$.chart).appendChild(this.chart_); + this.chart_.graphWidth = this.graphWidth; + this.chart_.graphHeight = this.graphHeight; + this.chart_.hideXAxis = true; + this.chart_.data = [ + { + x: '', + color: 'blue', + percentile_0: this.histogram.running.min, + percentile_25: this.histogram.getApproximatePercentile(0.25), + percentile_50: this.histogram.getApproximatePercentile(0.5), + percentile_75: this.histogram.getApproximatePercentile(0.75), + percentile_100: this.histogram.running.max, + } + ]; + } + this.$.stats_container.style.maxHeight = + this.chart_.getBoundingClientRect().height + 'px'; + await this.viewState.update({ + brushedBinRange: tr.b.math.Range.fromExplicitRange( + 0, this.histogram.allBins.length)}); + return; + } + + this.chart_ = new tr.ui.b.NameBarChart(); + Polymer.dom(this.$.chart).appendChild(this.chart_); + this.chart_.graphWidth = this.graphWidth; + this.chart_.graphHeight = this.graphHeight; + this.chart_.addEventListener('item-mousedown', + this.onMouseDown_.bind(this)); + this.chart_.addEventListener('item-mousemove', + this.onMouseMove_.bind(this)); + this.chart_.addEventListener('item-mouseup', + this.onMouseUp_.bind(this)); + this.chart_.hideLegend = true; + this.chart_.getDataSeries('y').color = 'blue'; + this.chart_.xAxisLabel = '#'; + this.chart_.brushedRange = this.viewState.brushedBinRange; + if (!this.viewState.brushedBinRange.isEmpty) { + this.updateDiagnostics_(); + } + + const chartData = []; + const binCounts = []; + for (const bin of this.histogram.allBins) { + let x = bin.range.min; + if (x === -Number.MAX_VALUE) { + x = '<' + new tr.b.Scalar( + this.histogram.unit, bin.range.max).toString(); + } else { + x = new tr.b.Scalar(this.histogram.unit, x).toString(); + } + chartData.push({x, y: bin.count}); + binCounts.push(bin.count); + } + + // If the largest 1 or 2 bins are more than twice as large as the next + // largest bin, then set the dataRange max to TRUNCATE_BIN_MARGIN% more + // than that next largest bin. + binCounts.sort((x, y) => y - x); + const dataRange = tr.b.math.Range.fromExplicitRange(0, binCounts[0]); + if (binCounts[1] > 0 && binCounts[0] > (binCounts[1] * 2)) { + dataRange.max = binCounts[1] * (1 + TRUNCATE_BIN_MARGIN); + } + if (binCounts[2] > 0 && binCounts[1] > (binCounts[2] * 2)) { + dataRange.max = binCounts[2] * (1 + TRUNCATE_BIN_MARGIN); + } + this.chart_.overrideDataRange = dataRange; + + this.chart_.data = chartData; + this.$.stats_container.style.maxHeight = + this.chart_.getBoundingClientRect().height + 'px'; + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html new file mode 100644 index 00000000000..2ca360b3348 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html @@ -0,0 +1,300 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/assert_utils.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/ui/histogram_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('basic', function() { + const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber); + h.addSample(-1, {foo: new tr.v.d.GenericSet(['a'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(0, {foo: new tr.v.d.GenericSet(['c'])}); + h.addSample(500, {foo: new tr.v.d.GenericSet(['c'])}); + h.addSample(999, {foo: new tr.v.d.GenericSet(['d'])}); + h.addSample(1000, {foo: new tr.v.d.GenericSet(['d'])}); + + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(h); + }); + + test('emptyHistogram', function() { + const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber); + + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(h); + }); + + test('viewBrushedBinRange', async function() { + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(tr.v.Histogram.create('a', tr.b.Unit.byName.count, + [0, 1, 2, 3, 4].map(value => { + return {value, diagnostics: new Map([ + ['i', new tr.v.d.GenericSet([value])]])}; + }))); + assert.isTrue(span.viewState.brushedBinRange.isEmpty); + + await span.viewState.update({ + brushedBinRange: tr.b.math.Range.fromExplicitRange(5, 10), + }); + const chart = tr.ui.b.findDeepElementMatchingPredicate( + span, e => e.tagName === 'svg'); + assert.strictEqual(5, chart.brushedRange.min); + assert.strictEqual(10, chart.brushedRange.max); + }); + + test('controlBrushedBinRange', async function() { + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(tr.v.Histogram.create('a', tr.b.Unit.byName.count, + [0, 1, 2, 3, 4])); + assert.isTrue(span.viewState.brushedBinRange.isEmpty); + + span.onMouseDown_({ + stopPropagation: () => undefined, + y: 21, + }); + span.onMouseUp_({ + stopPropagation: () => undefined, + y: 0, + }); + tr.b.assertRangeEquals(span.viewState.brushedBinRange, + tr.b.math.Range.fromExplicitRange(0, 22)); + }); + + test('viewMergeSampleDiagnostics', async function() { + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + const samples = []; + for (let i = 0; i < 5; ++i) { + samples.push({ + value: i, + diagnostics: { + breakdown: tr.v.d.Breakdown.fromDict({ + values: { + a: 5 - i, + b: i + 5, + c: i, + }, + }), + }, + }); + } + span.build(tr.v.Histogram.create('', tr.b.Unit.byName.count, samples)); + await span.viewState.update({brushedBinRange: + tr.b.math.Range.fromExplicitRange(0, 10)}); + const merge = tr.ui.b.findDeepElementMatchingPredicate(span, e => + e.id === 'merge_sample_diagnostics'); + assert.isTrue(merge.checked); + + await span.viewState.update({mergeSampleDiagnostics: false}); + assert.isFalse(merge.checked); + + await span.viewState.update({mergeSampleDiagnostics: true}); + assert.isTrue(merge.checked); + }); + + test('controlMergeSampleDiagnostics', async function() { + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + const samples = []; + for (let i = 0; i < 5; ++i) { + samples.push({ + value: i, + diagnostics: { + breakdown: tr.v.d.Breakdown.fromDict({ + values: { + a: 5 - i, + b: i + 5, + c: i, + }, + }), + }, + }); + } + span.build(tr.v.Histogram.create('', tr.b.Unit.byName.count, samples)); + await span.viewState.update({brushedBinRange: + tr.b.math.Range.fromExplicitRange(0, 10)}); + const merge = tr.ui.b.findDeepElementMatchingPredicate(span, e => + e.id === 'merge_sample_diagnostics'); + assert.isTrue(merge.checked); + + merge.click(); + assert.isFalse(span.viewState.mergeSampleDiagnostics); + + merge.click(); + assert.isTrue(span.viewState.mergeSampleDiagnostics); + }); + + test('mergeSampleDiagnostics', async function() { + // Add several samples with sample diagnostics to a Histogram, brush all of + // the bins, test that the sample diagnostics are merged. + const h = new tr.v.Histogram('', tr.b.Unit.byName.normalizedPercentage); + h.addSample(0.1, {foo: tr.v.d.Breakdown.fromDict({values: {a: 1, b: 2}})}); + h.addSample(0.3, {foo: tr.v.d.Breakdown.fromDict({values: {a: 3, b: 4}})}); + h.addSample(0.5, {foo: tr.v.d.Breakdown.fromDict({values: {a: 5, b: 6}})}); + h.addSample(0.7, {foo: tr.v.d.Breakdown.fromDict({values: {a: 7, b: 8}})}); + h.addSample(0.9, {foo: tr.v.d.Breakdown.fromDict({values: {a: 9, b: 10}})}); + + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(h); + await span.viewState.update({ + brushedBinRange: tr.b.math.Range.fromExplicitRange(0, h.allBins.length)}); + let breakdowns = tr.ui.b.findDeepElementsMatchingPredicate( + span, e => e.tagName === 'TR-V-UI-BREAKDOWN-SPAN'); + assert.lengthOf(breakdowns, 1); + + const merge = tr.ui.b.findDeepElementMatchingPredicate( + span, e => e.id === 'merge_sample_diagnostics'); + merge.click(); + breakdowns = tr.ui.b.findDeepElementsMatchingPredicate( + span, e => e.tagName === 'TR-V-UI-BREAKDOWN-SPAN'); + assert.lengthOf(breakdowns, 5); + }); + + test('cannotMergeSampleDiagnostics', async function() { + // Add several samples with sample diagnostics to a Histogram, brush all of + // the bins, test that the sample diagnostics are not merged. + const h = new tr.v.Histogram('', tr.b.Unit.byName.normalizedPercentage); + h.addSample(0.1, {foo: tr.v.d.Breakdown.fromDict({values: {a: 1, b: 2}})}); + h.addSample(0.3, {foo: tr.v.d.Breakdown.fromDict({values: {a: 3, b: 4}})}); + h.addSample(0.5, {foo: tr.v.d.Breakdown.fromDict({values: {a: 5, b: 6}})}); + h.addSample(0.7, {foo: tr.v.d.Breakdown.fromDict({values: {a: 7, b: 8}})}); + h.addSample(0.9, {foo: tr.v.d.Breakdown.fromDict({values: {a: 9, b: 10}})}); + + const span = document.createElement('tr-v-ui-histogram-span'); + span.canMergeSampleDiagnostics = false; + this.addHTMLOutput(span); + span.build(h); + await span.viewState.update({ + brushedBinRange: tr.b.math.Range.fromExplicitRange(0, h.allBins.length)}); + const breakdowns = tr.ui.b.findDeepElementsMatchingPredicate( + span, e => e.tagName === 'TR-V-UI-BREAKDOWN-SPAN'); + assert.lengthOf(breakdowns, 5); + }); + + test('singleSample', function() { + const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber); + h.addSample(100, { + sample_diagnostic_0: new tr.v.d.GenericSet(['foo']), + sample_diagnostic_1: new tr.v.d.GenericSet(['bar']), + }); + h.diagnostics.set('histogram diagnostic 0', new tr.v.d.GenericSet(['baz'])); + h.diagnostics.set('histogram diagnostic 1', new tr.v.d.GenericSet(['qux'])); + + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(h); + }); + + test('nans', function() { + const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber); + h.addSample(undefined, {foo: new tr.v.d.GenericSet(['b'])}); + h.addSample(NaN, {foo: new tr.v.d.GenericSet(['c'])}); + h.customizeSummaryOptions({nans: true}); + + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(h); + }); + + test('singleBin', function() { + const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber, + tr.v.HistogramBinBoundaries.SINGULAR); + h.addSample(0); + h.addSample(25); + h.addSample(100); + h.addSample(100); + h.addSample(25); + h.addSample(50); + h.addSample(75); + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(h); + }); + + test('referenceHistogram', function() { + const span = document.createElement('tr-v-ui-histogram-span'); + span.build(tr.v.Histogram.create('', tr.b.Unit.byName.count, [1, 10, 100], { + binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR, + }), tr.v.Histogram.create('', tr.b.Unit.byName.count, [2, 20, 200], { + binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR, + })); + this.addHTMLOutput(span); + }); + + test('breakdownUnit', async function() { + const root = new tr.v.Histogram('root', tr.b.Unit.byName.sizeInBytes); + const sampleBreakdown = new tr.v.d.Breakdown(); + sampleBreakdown.set('x', 30 << 20); + sampleBreakdown.set('y', 70 << 20); + root.addSample(100 << 20, {sampleBreakdown}); + const rhb = new tr.v.d.RelatedNameMap(); + root.diagnostics.set('rhb', rhb); + const aHist = new tr.v.Histogram('a', tr.b.Unit.byName.sizeInBytes); + rhb.set('a', aHist.name); + aHist.addSample(10 << 20); + const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.sizeInBytes); + rhb.set('b', bHist.name); + bHist.addSample(90 << 20); + const span = document.createElement('tr-v-ui-histogram-span'); + this.addHTMLOutput(span); + span.build(root); + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate( + span, e => e.textContent === '100.0 MiB')); + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate( + span, e => e.textContent === '30.0 MiB')); + assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate( + span, e => e.textContent === '70.0 MiB')); + }); + + test('diagnosticsTabs', async function() { + const span = document.createElement('tr-v-ui-histogram-span'); + span.build(tr.v.Histogram.create( + '', tr.b.Unit.byName.count, [ + {value: 1, diagnostics: new Map([ + ['sample diagnostic', new tr.v.d.GenericSet(['value1'])], + ])}, + {value: 10, diagnostics: new Map([ + ['sample diagnostic', new tr.v.d.GenericSet(['value10'])], + ])}, + ], { + diagnostics: new Map([ + [tr.v.d.RESERVED_NAMES.BENCHMARKS, new tr.v.d.GenericSet([ + 'system_health.common_desktop'])], + ]), + })); + this.addHTMLOutput(span); + + const sample = tr.ui.b.findDeepElementMatching( + span, '#sample_diagnostics_container'); + assert.strictEqual(span.rowState.diagnosticsTab, sample.tabLabel); + const tabs = tr.ui.b.findDeepElementMatching( + span, 'TR-UI-B-TAB-VIEW'); + assert.strictEqual(tabs.selectedSubView, sample); + + const metadata = tr.ui.b.findDeepElementMatching( + span, '#metadata_diagnostics'); + await span.rowState.update({diagnosticsTab: metadata.tabLabel}); + assert.strictEqual(tabs.selectedSubView, metadata); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html new file mode 100644 index 00000000000..1bc6ea696b3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html @@ -0,0 +1,353 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2018 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/math.html"> +<link rel="import" href="/tracing/base/math/running_statistics.html"> +<link rel="import" href="/tracing/ui/base/name_bar_chart.html"> +<link rel="import" href="/tracing/ui/base/name_column_chart.html"> +<dom-module id='tr-v-ui-metrics-visualization'> + <template> + <style> + button { + padding: 5px; + font-size: 14px; + } + + .text_input { + width: 50px; + padding: 4px; + font-size: 14px; + } + + .error { + color: red; + display: none; + } + + .container { + position: relative; + display: inline-block; + margin-left: 15px; + } + + #title { + font-size: 20px; + font-weight: bold; + padding-bottom: 5px; + } + + #selectors { + display: block; + padding-bottom: 10px; + } + + #search_page { + width: 200px; + margin-left: 30px; + } + + #close { + display: none; + vertical-align: top; + } + + #close svg{ + height: 1em; + } + + #close svg line { + stroke-width: 18; + stroke: black; + } + + #close:hover svg { + background: black; + } + + #close:hover svg line { + stroke: white; + } + </style> + <span id="aggregateContainer" class="container"> + </span> + <span id="pageByPageContainer" class="container"> + <span id="selectors"> + <span id="percentile_label">Percentile Range:</span> + <input id="start" class="text_input" placeholder="0"> + <input id="end" class="text_input" placeholder="100"> + <button id="filter" on-tap="filterByPercentile_">Filter</button> + <input id="search_page" class="text_input" placeholder="Page Name"> + <button id="search" on-tap="searchByPage_">Search</button> + <span id="search_error" class="error">Sorry, could not find that page!</span> + </span> + </span> + <div id="submetricsContainer" display="block"> + <span id="close"> + <svg viewbox="0 0 128 128"> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </svg> + </span> + </div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.v.ui', function() { + const PAGE_BREAKDOWN_KEY = 'pageBreakdown'; + + Polymer({ + is: 'tr-v-ui-metrics-visualization', + + created() { + this.charts_ = new Map(); + }, + + ready() { + this.$.start.addEventListener ('keydown', (e) => { + if (e.key === 'Enter') this.filterByPercentile_(); + }); + + this.$.end.addEventListener ('keydown', (e) => { + if (e.key === 'Enter') this.filterByPercentile_(); + }); + + this.$.search_page.addEventListener ('keydown', (e) => { + if (e.key === 'Enter') this.searchByPage_(); + }); + }, + + build(chartData) { + this.title_ = chartData.title; + this.aggregateData_ = chartData.aggregate; + this.data_ = chartData.page; + this.submetricsData_ = chartData.submetrics; + this.benchmarkCount_ = chartData.aggregate.length; + + // build aggregate chart + const aggregateChart = this.initializeColumnChart(this.title_); + Polymer.dom(this.$.aggregateContainer).appendChild(aggregateChart); + this.charts_.set(tr.v.ui.AGGREGATE_KEY, aggregateChart); + this.setChartColors_(tr.v.ui.AGGREGATE_KEY); + aggregateChart.data = chartData.aggregate; + this.setChartSize_(tr.v.ui.AGGREGATE_KEY); + + // build page by page + const newChart = this.initializeColumnChart(this.title_ + ' Breakdown'); + newChart.enableToolTip = true; + newChart.toolTipCallBack = (rect) => + this.openChildChart_(rect); + Polymer.dom(this.$.pageByPageContainer).appendChild(newChart); + this.charts_.set(PAGE_BREAKDOWN_KEY, newChart); + this.setChartColors_(PAGE_BREAKDOWN_KEY); + newChart.data = this.data_; + this.setChartSize_(PAGE_BREAKDOWN_KEY); + }, + + setChartSize_(page) { + const chart = this.charts_.get(page); + const pageCount = chart.data.length; + chart.graphHeight = tr.b.math.clamp(pageCount * 20, 400, 600); + chart.graphWidth = tr.b.math.clamp(pageCount * 30, 200, 1000); + }, + + // Assign color gradient to series in chart + setChartColors_(page) { + const chart = this.charts_.get(page); + const metrics = tr.v.ui.METRICS.get(this.title_); + for (let i = 0; i < this.benchmarkCount_; ++i) { + for (let j = 0; j < metrics.length; ++j) { + const mainColorIndex = j % tr.v.ui.COLORS.length; + const subColorIndex = i % tr.v.ui.COLORS[mainColorIndex].length; + const color = tr.v.ui.COLORS[mainColorIndex][subColorIndex]; + const series = metrics[j] + '-' + this.aggregateData_[i].x; + chart.getDataSeries(series).color = color; + if (i === 0) { + chart.getDataSeries(series).title = metrics[j]; + } else { + chart.getDataSeries(series).title = ''; + } + } + } + }, + + // Element creation + initializeColumnChart(title) { + const newChart = new tr.ui.b.NameColumnChart(); + newChart.hideLegend = false; + newChart.isStacked = true; + newChart.yAxisLabel = 'ms'; + newChart.hideXAxis = true; + newChart.displayXInHover = true; + newChart.isGrouped = true; + newChart.showTitleInLegend = true; + newChart.chartTitle = title; + newChart.titleHeight = '14pt'; + return newChart; + }, + + initializeChildChart_(title, height, width) { + const div = document.createElement('div'); + div.classList.add('container'); + Polymer.dom(this.$.submetricsContainer). + insertBefore(div, this.$.submetricsContainer.firstChild); + + const childChart = new tr.ui.b.NameBarChart(); + childChart.xAxisLabel = 'ms'; + childChart.chartTitle = title; + childChart.graphHeight = height; + childChart.graphWidth = width; + childChart.titleHeight = '14pt'; + childChart.isStacked = true; + childChart.hideLegend = true; + childChart.isGrouped = true; + childChart.isWaterfall = true; + + div.appendChild(childChart); + + const button = this.initializeCloseButton_(div, + this.$.submetricsContainer); + div.appendChild(button); + return childChart; + }, + + initializeCloseButton_(div, parent) { + const button = this.$.close.cloneNode(true); + button.style.display = 'inline-block'; + button.addEventListener ('click', () => { + Polymer.dom(parent).removeChild(div); + }); + return button; + }, + + // Create child chart and populate it + openChildChart_(rect) { + // Find main metric and corresponding sub-metrics + const metrics = tr.v.ui.METRICS.get(this.title_); + let metric; + let metricIndex; + for (let i = 0; i < metrics.length; ++i) { + if (rect.key.startsWith(metrics[i])) { + metric = metrics[i]; + metricIndex = i; + break; + } + } + + // Create child chart + const page = rect.datum.group; + const title = this.title_ + ' ' + metric + ': ' + page; + const submetrics = this.submetricsData_.get(page).get(metric); + const width = tr.b.math.clamp(submetrics.size * 150, 300, 700); + const height = tr.b.math.clamp(submetrics.size * + this.benchmarkCount_ * 50, 300, 700); + + const childChart = this.initializeChildChart_(title, height, width); + + // Get breakdown data for main step + childChart.data = this.processSubmetrics_(childChart, + submetrics, 0, metricIndex).data; + }, + + processSubmetrics_(chart, submetrics, hideValue, metricIndex) { + const finalData = []; + let submetricIndex = 0; + for (const submetric of submetrics.values()) { + let benchmarkIndex = 0; + for (const benchmark of submetric.values()) { + benchmark.hide = !hideValue ? 0 : hideValue; + const series = benchmark.x + '-' + benchmark.group; + const mainColorIndex = metricIndex % tr.v.ui.COLORS.length; + const subColorIndex = benchmarkIndex % + tr.v.ui.COLORS[mainColorIndex].length; + chart.getDataSeries(series).color = + tr.v.ui.COLORS[mainColorIndex][subColorIndex]; + if (benchmarkIndex === (this.benchmarkCount_ - 1)) { + hideValue += benchmark[series]; + } + finalData.push(benchmark); + benchmarkIndex++; + } + submetricIndex++; + } + return {data: finalData, hide: hideValue}; + }, + + // Handle filtering by start and end percentiles + filterByPercentile_() { + const startPercentile = this.$.start.value; + const endPercentile = this.$.end.value; + + if (startPercentile === '' || endPercentile === '') return; + + const length = this.data_.length / (this.benchmarkCount_ + 1); + const startIndex = this.getPercentileIndex_(startPercentile, length); + const endIndex = this.getPercentileIndex_(endPercentile, length); + this.charts_.get(PAGE_BREAKDOWN_KEY).data = + this.data_.slice(startIndex, endIndex); + }, + + // Get index of x percentile value + getPercentileIndex_(percentile, arrayLength) { + const index = Math.ceil(arrayLength * (percentile / 100.0)); + if (index === -1) return 0; + if (index >= arrayLength) return arrayLength; + return index * this.benchmarkCount_; + }, + + // Handle searching by page name + searchByPage_() { + const criteria = this.$.search_page.value; + if (criteria === '') return; + + const query = new RegExp(criteria); + + const filteredData = [...this.data_] + .filter(group => { + if (group.group) return group.group.match(query); + return false; + }); + + if (filteredData.length < 1) { + this.$.search_error.style.display = 'block'; + return; + } + + // Create child chart with breakdown data + const page = filteredData[0].group; + const title = this.title_ + ' Breakdown: ' + page; + const metricToSubmetricMap = this.submetricsData_.get(page); + + let totalSubmetrics = 0; + for (const submetrics of metricToSubmetricMap.values()) { + for (const benchmark of submetrics.values()) { + totalSubmetrics += benchmark.length; + } + } + const width = tr.b.math.clamp(totalSubmetrics * 150, 300, 700); + const height = tr.b.math.clamp(totalSubmetrics * + this.benchmarkCount_ * 30, 300, 700); + + const childChart = this.initializeChildChart_(title, height, width); + + const childData = []; + let hide = 0; + let metricIndex = 0; + for (const submetrics of metricToSubmetricMap.values()) { + const submetricsData = this.processSubmetrics_(childChart, submetrics, + hide, metricIndex); + childData.push(...submetricsData.data); + hide = submetricsData.hide; + metricIndex++; + } + childChart.data = childData; + }, + + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html new file mode 100644 index 00000000000..0d062557f63 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2018 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/ui/metrics_visualization.html"> +<link rel="import" href="/tracing/value/ui/visualizations_data_container.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(function() { + function generateChartBar(metrics, benchmark, page) { + const data = {x: benchmark, group: page}; + for (const metric of metrics) { + const key = metric + '-' + benchmark; + const mean = Math.random() * 100; + data[key] = Math.round(mean * 100) / 100; + } + return data; + } + + function generateSubmetricBar(submetric, benchmark, page, + metricToSubmetricMap) { + let submetricToBenchmarkMap = metricToSubmetricMap.get(submetric); + if (!submetricToBenchmarkMap) { + submetricToBenchmarkMap = []; + metricToSubmetricMap.set(submetric, submetricToBenchmarkMap); + } + const data = {x: submetric, hide: 0, group: benchmark}; + const mean = Math.random() * 100; + data[submetric + '-' + benchmark] = Math.round(mean * 100) / 100; + submetricToBenchmarkMap.push(data); + } + + test('instantiate', function() { + const mv = document.createElement('tr-v-ui-metrics-visualization'); + this.addHTMLOutput(mv); + + const testMetrics = tr.v.ui.METRICS.get('Thread'); + + // generate aggregate chart + const aggregateChart = []; + for (let i = 1; i <= 5; i++) { + aggregateChart.push(generateChartBar(testMetrics, + 'Run ' + i, 'aggregate')); + } + + // generate chart with individual page metrics + const chartData = []; + for (let i = 1; i <= 5; i++) { + for (let j = 1; j <= 5; j++) { + chartData.push(generateChartBar(testMetrics, + 'Run ' + i, 'Page ' + j)); + } + } + + // generate submetrics + const submetricsData = new Map(); + for (const metric in testMetrics) { + const testSubmetrics = [metric + 'a', metric + 'b', metric + 'c']; + for (let i = 1; i <= 5; i++) { + const page = 'Page ' + i; + const pageToMetricMap = tr.v.ui.getValueFromMap(page, + submetricsData); + const metricToSubmetricMap = tr.v.ui.getValueFromMap(metric, + pageToMetricMap); + for (let j = 1; j <= 5; j++) { + for (const submetric in testSubmetrics) { + generateSubmetricBar(submetric, 'Run ' + j, page, + metricToSubmetricMap); + } + } + } + } + + mv.build({ + title: 'Thread', + aggregate: aggregateChart, + page: chartData, + submetrics: submetricsData + }); + }); +}); +</script>
\ No newline at end of file diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html new file mode 100644 index 00000000000..b546041de50 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> + +<script> + 'use strict'; + Polymer({ + is: 'tr-v-ui-preferred-display-unit', + + ready() { + this.preferredTimeDisplayMode_ = undefined; + }, + + attached() { + tr.b.Unit.didPreferredTimeDisplayUnitChange(); + }, + + detached() { + tr.b.Unit.didPreferredTimeDisplayUnitChange(); + }, + + // null means no-preference + get preferredTimeDisplayMode() { + return this.preferredTimeDisplayMode_; + }, + + set preferredTimeDisplayMode(v) { + if (this.preferredTimeDisplayMode_ === v) return; + this.preferredTimeDisplayMode_ = v; + tr.b.Unit.didPreferredTimeDisplayUnitChange(); + } + + }); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html new file mode 100644 index 00000000000..382dc5963fe --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/time_display_modes.html"> +<link rel="import" href="/tracing/value/ui/preferred_display_unit.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const unit = document.createElement('tr-v-ui-preferred-display-unit'); + const ms = tr.b.TimeDisplayModes.ms; + unit.preferredDisplayUnit = ms; + assert.strictEqual(unit.preferredDisplayUnit, ms); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html new file mode 100644 index 00000000000..d3253d8eff9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html @@ -0,0 +1,274 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2018 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<dom-module id='tr-v-ui-raster-visualization'> + <template> + <style> + button { + padding: 5px; + font-size: 14px; + } + .error { + color: red; + display: none; + } + + .text_input { + width: 200px; + padding: 4px; + font-size: 14px; + } + + .selector_container{ + padding: 5px; + } + + #search { + display: inline-block; + padding-bottom: 10px; + } + + #search_page { + width: 200px; + } + + #pageSelector { + display: inline-block; + font-size: 12pt; + } + + #close { + display: none; + vertical-align: top; + } + + #close svg{ + height: 1em; + } + + #close svg line { + stroke-width: 18; + stroke: black; + } + + #close:hover svg { + background: black; + } + + #close:hover svg line { + stroke: white; + } + </style> + <span id="aggregateContainer"> + <div> + <div class="selector_container"> + <span id="select_page_label">Individual Page Results:</span> + <select id="pageSelector"> + <option id="select_page" value="">Select a page</option> + </select> + </div> + <div class="selector_container"> + <div id="search_page_label">Search for a page:</div> + <input id="search_page" class="text_input" placeholder="Page Name"> + <button id="search_button">Search</button> + <div id="search_error" class="error">Sorry, could not find that page!</div> + </div> + </div> + </span> + <span id="pageContainer"> + <span id="close"> + <svg viewbox="0 0 128 128"> + <line x1="28" y1="28" x2="100" y2="100"/> + <line x1="28" y1="100" x2="100" y2="28"/> + </svg> + </span> + </span> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-v-ui-raster-visualization', + + ready() { + this.$.pageSelector.addEventListener ('click', () => { + this.selectPage_(); + }); + + this.$.search_page.addEventListener ('keydown', (e) => { + if (e.key === 'Enter') this.searchByPage_(); + }); + + this.$.search_button.addEventListener ('click', () => { + this.searchByPage_(); + }); + }, + + + build(chartData) { + this.data_ = chartData; + const aggregateChart = this.createChart_('Aggregate Data by Run'); + Polymer.dom(this.$.aggregateContainer).appendChild(aggregateChart); + aggregateChart.enableToolTip = true; + aggregateChart.toolTipCallBack = (rect) => + this.openBenchmarkChart_(rect); + this.setChartColors_(aggregateChart, this.data_.get(tr.v.ui.AGGREGATE_KEY)); + aggregateChart.data = this.data_.get(tr.v.ui.AGGREGATE_KEY); + this.setChartSize_(aggregateChart, + this.data_.get(tr.v.ui.AGGREGATE_KEY).length); + + for (const page of this.data_.keys()) { + if (page === tr.v.ui.AGGREGATE_KEY) continue; + const option = document.createElement('option'); + option.textContent = page; + option.value = page; + this.$.pageSelector.appendChild(option); + } + }, + + setChartSize_(chart, pageCount, dataLength) { + chart.graphHeight = tr.b.math.clamp(pageCount * 25, 175, 1000); + chart.graphWidth = tr.b.math.clamp(pageCount * 25, 500, 1000); + }, + + setChartColors_(chart, data) { + const metrics = new Map(); + let count = 0; + for (const thread of tr.v.ui.FRAME.values()) { + for (const metric of thread.keys()) { + metrics.set(metric, count); + count++; + } + } + for (let i = 0; i < Math.floor(data.length / tr.v.ui.FRAME.length); ++i) { + let j = 0; + for (const [threadName, thread] of tr.v.ui.FRAME.entries()) { + for (const metric of thread.keys()) { + let color = 'transparent'; + if (thread.get(metric)) { + const mainColorIndex = metrics.get(metric) % tr.v.ui.COLORS.length; + const subColorIndex = i % tr.v.ui.COLORS[mainColorIndex].length; + color = tr.v.ui.COLORS[mainColorIndex][subColorIndex]; + } + const series = metric + '-' + data[i * 2 + j].x + '-' + threadName; + chart.getDataSeries(series).color = color; + chart.getDataSeries(series).title = !i ? metric : ''; + } + j++; + } + } + }, + + createChart_(title) { + const newChart = new tr.ui.b.NameBarChart(); + newChart.chartTitle = title; + newChart.xAxisLabel = 'ms'; + newChart.hideLegend = false; + newChart.showTitleInLegend = true; + newChart.hideYAxis = true; + newChart.isStacked = true; + newChart.displayXInHover = true; + newChart.isGrouped = true; + return newChart; + }, + + openBenchmarkChart_(rect) { + // Find main metric and corresponding sub-metrics + const benchmarkIndex = Math.floor(rect.index / tr.v.ui.FRAME.length); + const title = rect.datum.x; + + // Create child chart with breakdown data + const div = document.createElement('div'); + Polymer.dom(this.$.pageContainer). + insertBefore(div, this.$.pageContainer.firstChild); + + const chart = this.createChart_(title); + + div.appendChild(chart); + const button = this.initializeCloseButton_(div, this.$.pageContainer); + div.appendChild(button); + + const newDataSet = []; + + for (const page of this.data_.keys()) { + if (page === tr.v.ui.AGGREGATE_KEY) continue; + for (let i = 0; i < tr.v.ui.FRAME.length; i++) { + newDataSet.push(this.data_ + .get(page)[benchmarkIndex * tr.v.ui.FRAME.length + i]); + } + } + + this.setChartColors_(chart, newDataSet); + chart.data = newDataSet; + this.setChartSize_(chart, newDataSet.length); + }, + + selectPage_() { + // Create child chart with breakdown data + const div = document.createElement('div'); + const page = this.$.pageSelector.value; + if (page === '') return; + Polymer.dom(this.$.pageContainer). + insertBefore(div, this.$.pageContainer.firstChild); + + const pageChart = this.createChart_(page); + + div.appendChild(pageChart); + const button = this.initializeCloseButton_(div, this.$.pageContainer); + div.appendChild(button); + + const pageData = this.data_.get(page); + + this.setChartColors_(pageChart, pageData); + pageChart.data = pageData; + this.setChartSize_(pageChart, pageData.length); + }, + + searchByPage_() { + const criteria = this.$.search_page.value; + if (criteria === '') return; + + const query = new RegExp(criteria); + + const filteredData = [...this.data_.keys()] + .filter(page => page.match(query)); + + if (filteredData.length < 1) { + this.$.search_error.style.display = 'block'; + return; + } + + // Create child chart with breakdown data + const page = filteredData[0]; + + const div = document.createElement('div'); + Polymer.dom(this.$.pageContainer). + insertBefore(div, this.$.pageContainer.firstChild); + + const pageChart = this.createChart_(page); + + div.appendChild(pageChart); + const button = this.initializeCloseButton_(div, this.$.pageContainer); + div.appendChild(button); + + const pageData = this.data_.get(page); + + this.setChartColors_(pageChart, pageData); + pageChart.data = pageData; + this.setChartSize_(pageChart, pageData.length); + }, + + initializeCloseButton_(div, parent) { + const button = this.$.close.cloneNode(true); + button.style.display = 'inline-block'; + button.addEventListener('click', () => { + Polymer.dom(parent).removeChild(div); + }); + return button; + }, +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html new file mode 100644 index 00000000000..eaf3ee61842 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2018 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/ui/raster_visualization.html"> +<link rel="import" href="/tracing/value/ui/visualizations_data_container.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function generateBars(page, benchmark) { + const benchmarkData = []; + for (const [threadName, thread] of tr.v.ui.FRAME.entries()) { + const data = {x: benchmark, hide: 0}; + if (page !== tr.v.ui.AGGREGATE_KEY) data.group = page; + for (const metric of thread.keys()) { + const key = metric + '-' + data.x + '-' + threadName; + const mean = Math.random() * 100; + data[key] = Math.round(mean * 100) / 100; + } + benchmarkData.push(data); + } + return benchmarkData; + } + + test('instantiate', function() { + const rv = document.createElement('tr-v-ui-raster-visualization'); + this.addHTMLOutput(rv); + + const allChartData = new Map(); + + // generate aggregate data + let aggregateData = []; + for (let i = 1; i <= 5; i++) { + aggregateData = aggregateData.concat(generateBars(tr.v.ui.AGGREGATE_KEY, + 'Run ' + i)); + } + allChartData.set(tr.v.ui.AGGREGATE_KEY, aggregateData); + + // generate data per page + for (let i = 1; i <= 5; i++) { + const page = 'Page ' + i; + let chartData = []; + for (let j = 1; j <= 5; j++) { + chartData = chartData.concat(generateBars(page, 'Run ' + j)); + } + allChartData.set(page, chartData); + } + + rv.build(allChartData); + }); +}); +</script>
\ No newline at end of file diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html new file mode 100644 index 00000000000..ac52e51aba2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html"> + +<dom-module id="tr-v-ui-related-event-set-span"> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-related-event-set-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + updateContents_() { + Polymer.dom(this).textContent = ''; + const events = new tr.model.EventSet([...this.diagnostic]); + const link = document.createElement('tr-ui-a-analysis-link'); + let label = events.length + ' events'; + if (events.length === 1) { + const event = tr.b.getOnlyElement(events); + label = event.title + ' '; + label += tr.b.Unit.byName.timeDurationInMs.format( + event.duration); + } + link.setSelectionAndContent(events, label); + Polymer.dom(this).appendChild(link); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html new file mode 100644 index 00000000000..b0058a3c97b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/related_event_set.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate_RelatedEventSet0', function() { + const diagnostic = new tr.v.d.RelatedEventSet(); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-RELATED-EVENT-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + assert.strictEqual('0 events', span.textContent); + }); + + test('instantiate_RelatedEventSet1', function() { + const diagnostic = new tr.v.d.RelatedEventSet(); + tr.c.TestUtils.newModel(function(model) { + const proc = model.getOrCreateProcess(1); + const thread = proc.getOrCreateThread(2); + const event = tr.c.TestUtils.newSliceEx( + {title: 'a', start: 0, duration: 1}); + thread.sliceGroup.pushSlice(event); + diagnostic.add(event); + }); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-RELATED-EVENT-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + assert.strictEqual('a 1.000 ms', span.textContent); + }); + + test('instantiate_RelatedEventSet2', function() { + const diagnostic = new tr.v.d.RelatedEventSet(); + tr.c.TestUtils.newModel(function(model) { + const proc = model.getOrCreateProcess(1); + const thread = proc.getOrCreateThread(2); + let event = tr.c.TestUtils.newSliceEx({start: 0, duration: 1}); + thread.sliceGroup.pushSlice(event); + diagnostic.add(event); + event = tr.c.TestUtils.newSliceEx({start: 1, duration: 1}); + thread.sliceGroup.pushSlice(event); + diagnostic.add(event); + }); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-RELATED-EVENT-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + assert.strictEqual('2 events', span.textContent); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html new file mode 100644 index 00000000000..ac71cd7a1d7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html @@ -0,0 +1,204 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/event.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/raf.html"> + +<dom-module id="tr-v-ui-scalar-context-controller"> + <template></template> +</dom-module> + +<!-- +@fileoverview Polymer element for controlling common context across scalar +spans. To facilitate multiple separate contexts (e.g. a separate context for +each table column), each scalar span has to specify which "context group" +it belongs to: + + +============ some container element (e.g. <div>) ============+ + | | + | <tr-v-ui-scalar-context-controller> | + | ^ ^ | + | | | | + | v v | + | .... Context group 1 .... .... Context group 2 .... | + | : <tr-v-ui-scalar-span> : : <tr-v-ui-scalar-span> : | + | : <tr-v-ui-scalar-span> : : <tr-v-ui-scalar-span> : . . . | + | : . . . : : . . . : | + | :.......................: :.......................: | + +=============================================================+ + +An element can find its enclosing context controller using the +getScalarContextControllerForElement(node) defined in this file. Scalar spans +can push their state to the controller using the following three methods: + + 1. onScalarSpanAdded(contextGroup, span) + This method should be called when a span is attached to the DOM tree (or + afterwards when added to a context group). + + 2. onScalarSpanRemoved(contextGroup, span) + This method should be called when a span is detached from the DOM tree (or + beforehand when removed from a context group). + + 3. onScalarSpanUpdated(contextGroup, span) + This method should be called when the value of a span changes. + +Note: If a span wants to change its context group, it should first call +onScalarSpanRemoved with the old group and then onScalarSpanAdded with the new +group. + +If one or more group contexts are modified (due to one of the three methods +above), the controller will asynchronously (at the next RAF) update them and +fire a 'context-updated' event. Scalar spans can listen for this event and +update their UI accordingly. + +The context currently consists of the range of values of the associated spans. +This allows automatic display of relative sizes using sparklines. + +The controller design is based on: +https://docs.google.com/document/d/16ih8yYK8kF8MMlPnB-5KlyfS_AjjtbyAfi3pkxoZ8xs/edit?usp=sharing +--> +<script> +'use strict'; + +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-scalar-context-controller', + + created() { + this.host_ = undefined; + this.groupToContext_ = new Map(); + this.dirtyGroups_ = new Set(); + }, + + attached() { + if (this.host_) { + throw new Error( + 'Scalar context controller is already attached to a host'); + } + + const host = findParentOrHost(this); + if (host.__scalarContextController) { + throw new Error( + 'Multiple scalar context controllers attached to this host'); + } + + host.__scalarContextController = this; + this.host_ = host; + }, + + detached() { + if (!this.host_) { + throw new Error('Scalar context controller is not attached to a host'); + } + if (this.host_.__scalarContextController !== this) { + throw new Error( + 'Scalar context controller is not attached to its host'); + } + + delete this.host_.__scalarContextController; + this.host_ = undefined; + }, + + getContext(group) { + return this.groupToContext_.get(group); + }, + + onScalarSpanAdded(group, span) { + let context = this.groupToContext_.get(group); + if (context === undefined) { + context = { + spans: new Set(), + range: new tr.b.math.Range() + }; + this.groupToContext_.set(group, context); + } + if (context.spans.has(span)) { + throw new Error('Scalar span already registered with group: ' + group); + } + context.spans.add(span); + this.markGroupDirtyAndScheduleUpdate_(group); + }, + + onScalarSpanRemoved(group, span) { + const context = this.groupToContext_.get(group); + if (!context.spans.has(span)) { + throw new Error('Scalar span not registered with group: ' + group); + } + context.spans.delete(span); + this.markGroupDirtyAndScheduleUpdate_(group); + }, + + onScalarSpanUpdated(group, span) { + const context = this.groupToContext_.get(group); + if (!context.spans.has(span)) { + throw new Error('Scalar span not registered with group: ' + group); + } + this.markGroupDirtyAndScheduleUpdate_(group); + }, + + markGroupDirtyAndScheduleUpdate_(group) { + const alreadyDirty = this.dirtyGroups_.size > 0; + this.dirtyGroups_.add(group); + if (!alreadyDirty) { + tr.b.requestAnimationFrameInThisFrameIfPossible( + this.updateContext, this); + } + }, + + updateContext() { + const groups = this.dirtyGroups_; + if (groups.size === 0) return; + this.dirtyGroups_ = new Set(); + + for (const group of groups) { + this.updateGroup_(group); + } + + const event = new tr.b.Event('context-updated'); + event.groups = groups; + this.dispatchEvent(event); + }, + + updateGroup_(group) { + const context = this.groupToContext_.get(group); + if (context.spans.size === 0) { + this.groupToContext_.delete(group); + return; + } + context.range.reset(); + for (const span of context.spans) { + context.range.addValue(span.value); + } + } + }); + + function getScalarContextControllerForElement(element) { + while (element) { + if (element.__scalarContextController) { + return element.__scalarContextController; + } + element = findParentOrHost(element); + } + return undefined; + } + + function findParentOrHost(node) { + if (node.parentElement) { + return node.parentElement; + } + while (Polymer.dom(node).parentNode) { + node = Polymer.dom(node).parentNode; + } + return node.host; + } + + return { + getScalarContextControllerForElement, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html new file mode 100644 index 00000000000..ccb3f61d6f4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html @@ -0,0 +1,312 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/value/ui/scalar_context_controller.html"> + +<dom-module id="tr-v-ui-scalar-context-controller-mock-host"> + <template> + <tr-v-ui-scalar-context-controller id="controller"> + </tr-v-ui-scalar-context-controller> + <content></content> + </template> +</dom-module> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const getScalarContextControllerForElement = + tr.v.ui.getScalarContextControllerForElement; + + Polymer({ + is: 'tr-v-ui-scalar-context-controller-mock-host' + }); + + test('getScalarContextControllerForElement', function() { + const root = document.createElement('div'); + Polymer.dom(document.body).appendChild(root); + try { + assert.isUndefined(getScalarContextControllerForElement(root)); + + // <div> root + // |__<div> host1 + // |__<tr-v-ui-scalar-context-controller> c1 + const host1 = document.createElement('div'); + Polymer.dom(root).appendChild(host1); + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1)); + const c1 = document.createElement('tr-v-ui-scalar-context-controller'); + Polymer.dom(host1).appendChild(c1); + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + + // <div> root + // |__<div> host1 + // | |__<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // :..<tr-v-ui-scalar-context-controller> c2 + const host2 = document.createElement( + 'tr-v-ui-scalar-context-controller-mock-host'); + const c2 = host2.$.controller; + Polymer.dom(root).appendChild(host2); + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + assert.strictEqual(getScalarContextControllerForElement(host2), c2); + + // <div> root + // |__<div> host1 + // | |__<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // | :..<tr-v-ui-scalar-context-controller> c2 + // |__<div> divA + // |__<div> divB + const divA = document.createElement('div'); + Polymer.dom(host2).appendChild(divA); + assert.strictEqual(getScalarContextControllerForElement(divA), c2); + const divB = document.createElement('div'); + Polymer.dom(divA).appendChild(divB); + assert.strictEqual(getScalarContextControllerForElement(divB), c2); + + // <div> root + // |__<div> host1 + // | |_<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // | :.-<tr-v-ui-scalar-context-controller> c2 + // |__<div> divA + // |__<div> divB + // |__<tr-v-ui-scalar-context-controller-mock-host> host3 + // :..<tr-v-ui-scalar-context-controller> c3 + const host3 = document.createElement( + 'tr-v-ui-scalar-context-controller-mock-host'); + Polymer.dom(divB).appendChild(host3); + const c3 = host3.$.controller; + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + assert.strictEqual(getScalarContextControllerForElement(host2), c2); + assert.strictEqual(getScalarContextControllerForElement(divA), c2); + assert.strictEqual(getScalarContextControllerForElement(divB), c2); + assert.strictEqual(getScalarContextControllerForElement(host3), c3); + + // <div> root + // |__<div> host1 + // | |_<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // | :.-<tr-v-ui-scalar-context-controller> c2 + // |__<div> divA + // | :.<tr-v-ui-scalar-context-controller> c4 + // |__<div> divB + // |__<tr-v-ui-scalar-context-controller-mock-host> host3 + // :..<tr-v-ui-scalar-context-controller> c3 + const c4 = document.createElement('tr-v-ui-scalar-context-controller'); + Polymer.dom(divA).appendChild(c4); + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + assert.strictEqual(getScalarContextControllerForElement(host2), c2); + assert.strictEqual(getScalarContextControllerForElement(divA), c4); + assert.strictEqual(getScalarContextControllerForElement(divB), c4); + assert.strictEqual(getScalarContextControllerForElement(host3), c3); + + // <div> root + // |__<div> host1 + // | |_<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // | :.-<tr-v-ui-scalar-context-controller> c2 + // |__<div> divA + // | :.<tr-v-ui-scalar-context-controller> c4 + // |__<div> divB + // |__<tr-v-ui-scalar-context-controller-mock-host> host3 + Polymer.dom(host3.root).removeChild(c3); + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + assert.strictEqual(getScalarContextControllerForElement(host2), c2); + assert.strictEqual(getScalarContextControllerForElement(divA), c4); + assert.strictEqual(getScalarContextControllerForElement(divB), c4); + assert.strictEqual(getScalarContextControllerForElement(host3), c4); + + // <div> root + // |__<div> host1 + // | |_<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // |__<div> divA + // | :.<tr-v-ui-scalar-context-controller> c4 + // |__<div> divB + // |__<tr-v-ui-scalar-context-controller-mock-host> host3 + Polymer.dom(host2.root).removeChild(c2); + assert.isUndefined(getScalarContextControllerForElement(root)); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + assert.isUndefined(getScalarContextControllerForElement(host2)); + assert.strictEqual(getScalarContextControllerForElement(divA), c4); + assert.strictEqual(getScalarContextControllerForElement(divB), c4); + assert.strictEqual(getScalarContextControllerForElement(host3), c4); + + // <div> root + // | :.<tr-v-ui-scalar-context-controller> c3 + // |__<div> host1 + // | |_<tr-v-ui-scalar-context-controller> c1 + // |__<tr-v-ui-scalar-context-controller-mock-host> host2 + // |__<div> divA + // | :.<tr-v-ui-scalar-context-controller> c4 + // |__<div> divB + // |__<tr-v-ui-scalar-context-controller-mock-host> host3 + Polymer.dom(root).appendChild(c3); + assert.strictEqual(getScalarContextControllerForElement(root), c3); + assert.strictEqual(getScalarContextControllerForElement(host1), c1); + assert.strictEqual(getScalarContextControllerForElement(host2), c3); + assert.strictEqual(getScalarContextControllerForElement(divA), c4); + assert.strictEqual(getScalarContextControllerForElement(divB), c4); + assert.strictEqual(getScalarContextControllerForElement(host3), c4); + } finally { + Polymer.dom(document.body).removeChild(root); + } + }); + + function contextTest(name, testCallback) { + test('context_' + name, function() { + const root = document.createElement('div'); + Polymer.dom(document.body).appendChild(root); + try { + const c = document.createElement('tr-v-ui-scalar-context-controller'); + Polymer.dom(root).appendChild(c); + + let updatedGroups = []; // Fail if event fires unexpectedly. + c.addEventListener('context-updated', function(e) { + if (updatedGroups) { + assert.fail('Unexpected context-updated event fired.'); + } + updatedGroups = Array.from(e.groups); + }); + + c.expectContextUpdatedEventForTesting = + function(expectedUpdatedGroups) { + updatedGroups = undefined; + tr.b.forceAllPendingTasksToRunForTest(); + assert.sameMembers(updatedGroups, expectedUpdatedGroups); + }; + + testCallback.call(this, c); + } finally { + Polymer.dom(document.body).removeChild(root); + } + }); + } + + contextTest('singleGroup', function(c) { + assert.isUndefined(c.getContext('G')); + + const s1 = {value: 10}; + c.onScalarSpanAdded('G', s1); + c.expectContextUpdatedEventForTesting(['G']); + assert.isTrue(c.getContext('G').range.equals( + tr.b.math.Range.fromExplicitRange(10, 10))); + assert.sameMembers(Array.from(c.getContext('G').spans), [s1]); + + const s2 = {value: 15}; + c.onScalarSpanAdded('G', s2); + c.expectContextUpdatedEventForTesting(['G']); + assert.isTrue(c.getContext('G').range.equals( + tr.b.math.Range.fromExplicitRange(10, 15))); + assert.sameMembers(Array.from(c.getContext('G').spans), [s1, s2]); + + s1.value = 5; + c.onScalarSpanUpdated('G', s1); + c.expectContextUpdatedEventForTesting(['G']); + assert.isTrue(c.getContext('G').range.equals( + tr.b.math.Range.fromExplicitRange(5, 15))); + assert.sameMembers(Array.from(c.getContext('G').spans), [s1, s2]); + + c.onScalarSpanRemoved('G', s2); + c.expectContextUpdatedEventForTesting(['G']); + assert.isTrue(c.getContext('G').range.equals( + tr.b.math.Range.fromExplicitRange(5, 5))); + assert.sameMembers(Array.from(c.getContext('G').spans), [s1]); + + const s3 = {value: 0}; + c.onScalarSpanAdded('G', s3); + s2.value = 14; + c.onScalarSpanAdded('G', s2); + c.expectContextUpdatedEventForTesting(['G']); + assert.isTrue(c.getContext('G').range.equals( + tr.b.math.Range.fromExplicitRange(0, 14))); + assert.sameMembers(Array.from(c.getContext('G').spans), [s1, s2, s3]); + + c.onScalarSpanRemoved('G', s1); + c.onScalarSpanRemoved('G', s2); + c.onScalarSpanRemoved('G', s3); + c.expectContextUpdatedEventForTesting(['G']); + assert.isUndefined(c.getContext('G')); + + c.onScalarSpanAdded('G', s2); + c.expectContextUpdatedEventForTesting(['G']); + assert.isTrue(c.getContext('G').range.equals( + tr.b.math.Range.fromExplicitRange(14, 14))); + assert.sameMembers(Array.from(c.getContext('G').spans), [s2]); + }); + + contextTest('multipleGroups', function(c) { + assert.isUndefined(c.getContext('G1')); + assert.isUndefined(c.getContext('G2')); + + const s1 = {value: 0}; + c.onScalarSpanAdded('G1', s1); + c.expectContextUpdatedEventForTesting(['G1']); + assert.isTrue(c.getContext('G1').range.equals( + tr.b.math.Range.fromExplicitRange(0, 0))); + assert.sameMembers(Array.from(c.getContext('G1').spans), [s1]); + + const s2 = {value: 1}; + c.onScalarSpanAdded('G2', s2); + c.expectContextUpdatedEventForTesting(['G2']); + assert.isTrue(c.getContext('G2').range.equals( + tr.b.math.Range.fromExplicitRange(1, 1))); + assert.sameMembers(Array.from(c.getContext('G2').spans), [s2]); + + const s3 = {value: 2}; + const s4 = {value: -1}; + c.onScalarSpanAdded('G2', s3); + c.onScalarSpanAdded('G1', s4); + c.expectContextUpdatedEventForTesting(['G1', 'G2']); + assert.isTrue(c.getContext('G1').range.equals( + tr.b.math.Range.fromExplicitRange(-1, 0))); + assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s4]); + assert.isTrue(c.getContext('G2').range.equals( + tr.b.math.Range.fromExplicitRange(1, 2))); + assert.sameMembers(Array.from(c.getContext('G2').spans), [s2, s3]); + + c.onScalarSpanRemoved('G2', s3); + c.onScalarSpanAdded('G1', s3); + c.expectContextUpdatedEventForTesting(['G1', 'G2']); + assert.isTrue(c.getContext('G1').range.equals( + tr.b.math.Range.fromExplicitRange(-1, 2))); + assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s3, s4]); + assert.isTrue(c.getContext('G2').range.equals( + tr.b.math.Range.fromExplicitRange(1, 1))); + assert.sameMembers(Array.from(c.getContext('G2').spans), [s2]); + + s4.value = 3; + c.onScalarSpanUpdated('G1', s4); + s1.value = 1; + c.onScalarSpanUpdated('G1', s1); + c.expectContextUpdatedEventForTesting(['G1']); + assert.isTrue(c.getContext('G1').range.equals( + tr.b.math.Range.fromExplicitRange(1, 3))); + assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s3, s4]); + assert.isTrue(c.getContext('G2').range.equals( + tr.b.math.Range.fromExplicitRange(1, 1))); + assert.sameMembers(Array.from(c.getContext('G2').spans), [s2]); + + c.onScalarSpanRemoved('G2', s2); + c.expectContextUpdatedEventForTesting(['G2']); + assert.isTrue(c.getContext('G1').range.equals( + tr.b.math.Range.fromExplicitRange(1, 3))); + assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s3, s4]); + assert.isUndefined(c.getContext('G2')); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html new file mode 100644 index 00000000000..631c3696f8c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id="tr-v-ui-scalar-diagnostic-span"> + <template> + <tr-v-ui-scalar-span id="scalar"></tr-v-ui-scalar-span> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-scalar-diagnostic-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + updateContents_() { + this.$.scalar.setValueAndUnit(this.diagnostic.value.value, + this.diagnostic.value.unit); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html new file mode 100644 index 00000000000..9f6167f1acb --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/diagnostics/scalar.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const diagnostic = new tr.v.d.Scalar(new tr.b.Scalar( + tr.b.Unit.byName.timeDurationInMs, 123.456)); + const span = tr.v.ui.createDiagnosticSpan(diagnostic); + assert.strictEqual('TR-V-UI-SCALAR-DIAGNOSTIC-SPAN', span.tagName); + this.addHTMLOutput(span); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html new file mode 100644 index 00000000000..24a5cf3ac0f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id="tr-v-ui-scalar-map-table"> + <template> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> + +<script> +'use strict'; +Polymer({ + is: 'tr-v-ui-scalar-map-table', + + created() { + /** @type {!Map.<string, !tr.b.Scalar>} */ + this.scalarMap_ = new Map(); + + /** @type {!Map.<string, !tr.b.math.Statistics.Significance>} */ + this.significance_ = new Map(); + }, + + ready() { + this.$.table.showHeader = false; + this.$.table.tableColumns = [ + { + value(row) { + return row.name; + } + }, + { + value(row) { + const span = tr.v.ui.createScalarSpan(row.value); + if (row.significance !== undefined) { + span.significance = row.significance; + } else if (row.anyRowsHaveSignificance) { + // Ensure vertical alignment. + span.style.marginRight = '18px'; + } + span.style.whiteSpace = 'nowrap'; + return span; + } + } + ]; + }, + + get scalarMap() { + return this.scalarMap_; + }, + + /** + * @param {!Map.<string,!tr.b.Scalar>} map + */ + set scalarMap(map) { + this.scalarMap_ = map; + this.updateContents_(); + }, + + /** + * @param {string} key + * @param {!tr.b.math.Statistics.Significance} significance + */ + setSignificanceForKey(key, significance) { + this.significance_.set(key, significance); + this.updateContents_(); + }, + + updateContents_() { + const rows = []; + for (const [key, scalar] of this.scalarMap) { + rows.push({ + name: key, + value: scalar, + significance: this.significance_.get(key), + anyRowsHaveSignificance: (this.significance_.size > 0) + }); + } + this.$.table.tableRows = rows; + this.$.table.rebuild(); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html new file mode 100644 index 00000000000..4c9a50c2314 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/ui/scalar_map_table.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const span = document.createElement('tr-v-ui-scalar-map-table'); + + const histogram = new tr.v.Histogram('', tr.b.Unit.byName.energyInJoules); + for (let i = 0; i < 1e2; ++i) { + histogram.addSample(Math.random() * 1000); + } + + histogram.addSample('foo'); + histogram.customizeSummaryOptions({nans: true}); + + span.scalarMap = histogram.statisticsScalars; + this.addHTMLOutput(span); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html new file mode 100644 index 00000000000..50d89653ed1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html @@ -0,0 +1,626 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/ui/scalar_context_controller.html"> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + /** + * One common simple way to use this function is + * createScalarSpan(number, {unit: tr.b.Unit.byName.whatever}) + * + * This function can also take a Scalar, undefined, or a Histogram plus + * significance, contextGroup, customContextRange, leftAlign and/or inline. + * + * @param {undefined|tr.b.Scalar|tr.v.Histogram} value + * @param {Object=} opt_config + * @param {!tr.b.math.Range=} opt_config.customContextRange + * @param {boolean=} opt_config.leftAlign + * @param {boolean=} opt_config.inline + * @param {!tr.b.Unit=} opt_config.unit + * @param {tr.b.math.Statistics.Significance=} opt_config.significance + * @param {string=} opt_config.contextGroup + * @return {(string|!HTMLElement)} + */ + function createScalarSpan(value, opt_config) { + if (value === undefined) return ''; + + const config = opt_config || {}; + const ownerDocument = config.ownerDocument || document; + + const span = ownerDocument.createElement('tr-v-ui-scalar-span'); + + let numericValue; + if (value instanceof tr.b.Scalar) { + span.value = value; + numericValue = value.value; + } else if (value instanceof tr.v.Histogram) { + numericValue = value.average; + if (numericValue === undefined) return ''; + span.setValueAndUnit(numericValue, value.unit); + } else { + const unit = config.unit; + if (unit === undefined) { + throw new Error( + 'Unit must be provided in config when value is a number'); + } + span.setValueAndUnit(value, unit); + numericValue = value; + } + + if (config.context) { + span.context = config.context; + } + + if (config.customContextRange) { + span.customContextRange = config.customContextRange; + } + + if (config.leftAlign) { + span.leftAlign = true; + } + + if (config.inline) { + span.inline = true; + } + + if (config.significance !== undefined) { + span.significance = config.significance; + } + + if (config.contextGroup !== undefined) { + span.contextGroup = config.contextGroup; + } + + return span; + } + + return { + createScalarSpan, + }; +}); +</script> + +<dom-module id="tr-v-ui-scalar-span"> + <template> + <style> + :host { + display: flex; + flex-direction: row; + justify-content: flex-end; + position: relative; + /* Limit the sparkline's negative z-index to the span only. */ + isolation: isolate; + } + + :host(.left-align) { + justify-content: flex-start; + } + + :host(.inline) { + display: inline-flex; + } + + #sparkline { + width: 0%; + position: absolute; + bottom: 0; + display: none; + height: 100%; + background-color: hsla(216, 100%, 94.5%, .75); + border-color: hsl(216, 100%, 89%); + box-sizing: border-box; + z-index: -1; + } + #sparkline.positive { + border-right-style: solid; + /* The border width must be kept in sync with buildSparklineStyle_(). */ + border-right-width: 1px; + } + #sparkline:not(.positive) { + border-left-style: solid; + /* The border width must be kept in sync with buildSparklineStyle_(). */ + border-left-width: 1px; + } + #sparkline.better { + background-color: hsla(115, 100%, 93%, .75); + border-color: hsl(118, 60%, 80%); + } + #sparkline.worse { + background-color: hsla(0, 100%, 88%, .75); + border-color: hsl(0, 100%, 80%); + } + + #content { + white-space: nowrap; + } + #content, #significance, #warning { + flex-grow: 0; + } + #content.better { + color: green; + } + #content.worse { + color: red; + } + + #significance svg { + margin-left: 4px; + display: none; + height: 1em; + vertical-align: text-top; + stroke-width: 4; + fill: rgba(0, 0, 0, 0); + } + #significance #insignificant { + stroke: black; + } + #significance #significantly_better { + stroke: green; + } + #significance #significantly_worse { + stroke: red; + } + + #warning { + display: none; + margin-left: 4px; + height: 1em; + vertical-align: text-top; + stroke-width: 0; + } + #warning path { + fill: rgb(255, 185, 185); + } + #warning rect { + fill: red; + } + </style> + + <span id="sparkline"></span> + + <span id="content"></span> + + <span id="significance"> + <!-- Neutral face --> + <svg viewbox="0 0 128 128" id="insignificant"> + <circle r="60" cx="64" cy="64"/> + <circle r="4" cx="44" cy="44"/> + <circle r="4" cx="84" cy="44"/> + <line x1="36" x2="92" y1="80" y2="80"/> + </svg> + + <!-- Smiling face --> + <svg viewbox="0 0 128 128" id="significantly_better"> + <circle r="60" cx="64" cy="64"/> + <circle r="4" cx="44" cy="44"/> + <circle r="4" cx="84" cy="44"/> + <path d="M 28 64 Q 64 128 100 64"/> + </svg> + + <!-- Frowning face --> + <svg viewbox="0 0 128 128" id="significantly_worse"> + <circle r="60" cx="64" cy="64"/> + <circle r="4" cx="44" cy="44"/> + <circle r="4" cx="84" cy="44"/> + <path d="M 36 96 Q 64 48 92 96"/> + </svg> + </span> + + <svg viewbox="0 0 128 128" id="warning"> + <path d="M 64 0 L 128 128 L 0 128 L 64 0"/> + <rect x="60" width="8" y="0" height="84"/> + <rect x="60" width="8" y="100" height="24"/> + </svg> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-v-ui-scalar-span', + + properties: { + /** + * String identifier for grouping scalar spans with common context (e.g. + * all scalar spans in a single table column would typically share a common + * context and, thus, have the same context group identifier). If falsy, + * the scalar span will NOT be associated with any context. + */ + contextGroup: { + type: String, + reflectToAttribute: true, + observer: 'contextGroupChanged_' + } + }, + + created() { + this.value_ = undefined; + this.unit_ = undefined; + + // TODO(petrcermak): Merge this into the context controller. + this.context_ = undefined; + + this.warning_ = undefined; + this.significance_ = tr.b.math.Statistics.Significance.DONT_CARE; + + // To avoid unnecessary DOM traversal, search for the context controller + // only when necessary (when the span is attached and has a context group). + this.shouldSearchForContextController_ = false; + this.lazyContextController_ = undefined; + this.onContextUpdated_ = this.onContextUpdated_.bind(this); + this.updateContents_ = this.updateContents_.bind(this); + + // The span can specify a custom context range, which will override the + // values from the context controller. + this.customContextRange_ = undefined; + }, + + get significance() { + return this.significance_; + }, + + set significance(s) { + this.significance_ = s; + this.updateContents_(); + }, + + set contentTextDecoration(deco) { + this.$.content.style.textDecoration = deco; + }, + + get value() { + return this.value_; + }, + + set value(value) { + if (value instanceof tr.b.Scalar) { + this.value_ = value.value; + this.unit_ = value.unit; + } else { + this.value_ = value; + } + this.updateContents_(); + if (this.hasContext_(this.contextGroup)) { + this.contextController_.onScalarSpanUpdated(this.contextGroup, this); + } else { + this.updateSparkline_(); + } + }, + + get contextController_() { + if (this.shouldSearchForContextController_) { + this.lazyContextController_ = + tr.v.ui.getScalarContextControllerForElement(this); + this.shouldSearchForContextController_ = false; + } + return this.lazyContextController_; + }, + + hasContext_(contextGroup) { + // The ordering here is important. It ensures that we avoid a DOM traversal + // when the span doesn't have a context group. + return !!(contextGroup && this.contextController_); + }, + + contextGroupChanged_(newContextGroup, oldContextGroup) { + this.detachFromContextControllerIfPossible_(oldContextGroup); + if (!this.attachToContextControllerIfPossible_(newContextGroup)) { + // If the span failed to attach to a controller, it won't receive a + // context-updated event, so we trigger it manually. + this.onContextUpdated_(); + } + }, + + attachToContextControllerIfPossible_(contextGroup) { + if (!this.hasContext_(contextGroup)) return false; + + this.contextController_.addEventListener( + 'context-updated', this.onContextUpdated_); + this.contextController_.onScalarSpanAdded(contextGroup, this); + return true; + }, + + detachFromContextControllerIfPossible_(contextGroup) { + if (!this.hasContext_(contextGroup)) return; + + this.contextController_.removeEventListener( + 'context-updated', this.onContextUpdated_); + this.contextController_.onScalarSpanRemoved(contextGroup, this); + }, + + attached() { + tr.b.Unit.addEventListener( + 'display-mode-changed', this.updateContents_); + this.shouldSearchForContextController_ = true; + this.attachToContextControllerIfPossible_(this.contextGroup); + }, + + detached() { + tr.b.Unit.removeEventListener( + 'display-mode-changed', this.updateContents_); + this.detachFromContextControllerIfPossible_(this.contextGroup); + this.shouldSearchForContextController_ = false; + this.lazyContextController_ = undefined; + }, + + onContextUpdated_() { + this.updateSparkline_(); + }, + + get context() { + return this.context_; + }, + + set context(context) { + this.context_ = context; + this.updateContents_(); + }, + + get unit() { + return this.unit_; + }, + + set unit(unit) { + this.unit_ = unit; + this.updateContents_(); + this.updateSparkline_(); + }, + + setValueAndUnit(value, unit) { + this.value_ = value; + this.unit_ = unit; + this.updateContents_(); + }, + + get customContextRange() { + return this.customContextRange_; + }, + + set customContextRange(customContextRange) { + this.customContextRange_ = customContextRange; + this.updateSparkline_(); + }, + + get inline() { + return Polymer.dom(this).classList.contains('inline'); + }, + + set inline(inline) { + if (inline) { + Polymer.dom(this).classList.add('inline'); + } else { + Polymer.dom(this).classList.remove('inline'); + } + }, + + get leftAlign() { + return Polymer.dom(this).classList.contains('left-align'); + }, + + set leftAlign(leftAlign) { + if (leftAlign) { + Polymer.dom(this).classList.add('left-align'); + } else { + Polymer.dom(this).classList.remove('left-align'); + } + }, + + updateSparkline_() { + Polymer.dom(this.$.sparkline).classList.remove('positive'); + Polymer.dom(this.$.sparkline).classList.remove('better'); + Polymer.dom(this.$.sparkline).classList.remove('worse'); + Polymer.dom(this.$.sparkline).classList.remove('same'); + this.$.sparkline.style.display = 'none'; + this.$.sparkline.style.left = '0'; + this.$.sparkline.style.width = '0'; + + // Custom context range takes precedence over controller context range. + let range = this.customContextRange_; + if (!range && this.hasContext_(this.contextGroup)) { + const context = this.contextController_.getContext(this.contextGroup); + if (context) { + range = context.range; + } + } + if (!range || range.isEmpty) return; + + const leftPoint = Math.min(range.min, 0); + const rightPoint = Math.max(range.max, 0); + const pointDistance = rightPoint - leftPoint; + if (pointDistance === 0) { + // This can happen, for example, when all spans within the context have + // zero values (so |range| is [0, 0]). + return; + } + + // Draw the sparkline. + this.$.sparkline.style.display = 'block'; + let left; + let width; + if (this.value > 0) { + width = Math.min(this.value, rightPoint); + left = -leftPoint; + Polymer.dom(this.$.sparkline).classList.add('positive'); + } else if (this.value <= 0) { + width = -Math.max(this.value, leftPoint); + left = (-leftPoint) - width; + } + this.$.sparkline.style.left = this.buildSparklineStyle_( + left / pointDistance, false); + this.$.sparkline.style.width = this.buildSparklineStyle_( + width / pointDistance, true); + + // Set the sparkline color (if applicable). + const changeClass = this.changeClassName_; + if (changeClass) { + Polymer.dom(this.$.sparkline).classList.add(changeClass); + } + }, + + buildSparklineStyle_(ratio, isWidth) { + // To avoid visual glitches around the zero value bar, we subtract 1 pixel + // from the width of the element and multiply the remainder (100% - 1px) by + // |ratio|. The extra pixel is used for the sparkline border. This allows + // us to align zero sparklines with both positive and negative values: + // + // ::::::::::| +10 MiB + // :::::| +5 MiB + // | 0 MiB + // |::::: -5 MiB + // |:::::::::: -10 MiB + // + let position = 'calc(' + ratio + ' * (100% - 1px)'; + if (isWidth) { + position += ' + 1px'; // Extra pixel for sparkline border. + } + position += ')'; + return position; + }, + + updateContents_() { + Polymer.dom(this.$.content).textContent = ''; + Polymer.dom(this.$.content).classList.remove('better'); + Polymer.dom(this.$.content).classList.remove('worse'); + Polymer.dom(this.$.content).classList.remove('same'); + this.$.insignificant.style.display = ''; + this.$.significantly_better.style.display = ''; + this.$.significantly_worse.style.display = ''; + + if (this.unit_ === undefined) return; + + this.$.content.title = ''; + Polymer.dom(this.$.content).textContent = + this.unit_.format(this.value, this.context); + this.updateDelta_(); + }, + + updateDelta_() { + let changeClass = this.changeClassName_; + if (!changeClass) { + this.$.significance.style.display = 'none'; + return; // Not a delta or we don't care. + } + + this.$.significance.style.display = 'inline'; + + let title; + switch (changeClass) { + case 'better': + title = 'improvement'; + break; + + case 'worse': + title = 'regression'; + break; + + case 'same': + title = 'no change'; + break; + + default: + throw new Error('Unknown change class: ' + changeClass); + } + + // Set the content class separately from the significance class so that + // the Neutral face is always a neutral color. + Polymer.dom(this.$.content).classList.add(changeClass); + + switch (this.significance) { + case tr.b.math.Statistics.Significance.DONT_CARE: + break; + + case tr.b.math.Statistics.Significance.INSIGNIFICANT: + if (changeClass !== 'same') title = 'insignificant ' + title; + this.$.insignificant.style.display = 'inline'; + changeClass = 'same'; + break; + + case tr.b.math.Statistics.Significance.SIGNIFICANT: + if (changeClass === 'same') { + throw new Error('How can no change be significant?'); + } + this.$['significantly_' + changeClass].style.display = 'inline'; + title = 'significant ' + title; + break; + + default: + throw new Error('Unknown significance ' + this.significance); + } + + this.$.significance.title = title; + this.$.content.title = title; + }, + + get changeClassName_() { + if (!this.unit_ || !this.unit_.isDelta) return undefined; + + switch (this.unit_.improvementDirection) { + case tr.b.ImprovementDirection.DONT_CARE: + return undefined; + + case tr.b.ImprovementDirection.BIGGER_IS_BETTER: + if (this.value === 0) return 'same'; + return this.value > 0 ? 'better' : 'worse'; + + case tr.b.ImprovementDirection.SMALLER_IS_BETTER: + if (this.value === 0) return 'same'; + return this.value < 0 ? 'better' : 'worse'; + + default: + throw new Error('Unknown improvement direction: ' + + this.unit_.improvementDirection); + } + }, + + get warning() { + return this.warning_; + }, + + set warning(warning) { + this.warning_ = warning; + const warningEl = this.$.warning; + if (this.warning_) { + warningEl.title = warning; + warningEl.style.display = 'inline'; + } else { + warningEl.title = ''; + warningEl.style.display = ''; + } + }, + + // tr-v-ui-time-stamp-span property + get timestamp() { + return this.value; + }, + + set timestamp(timestamp) { + if (timestamp instanceof tr.b.u.TimeStamp) { + this.value = timestamp; + return; + } + this.setValueAndUnit(timestamp, tr.b.u.Units.timeStampInMs); + }, + + // tr-v-ui-time-duration-span property + get duration() { + return this.value; + }, + + set duration(duration) { + if (duration instanceof tr.b.u.TimeDuration) { + this.value = duration; + return; + } + this.setValueAndUnit(duration, tr.b.u.Units.timeDurationInMs); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html new file mode 100644 index 00000000000..56ca6917d71 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html @@ -0,0 +1,1027 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/base/time_display_modes.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/unit_scale.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/ui/scalar_context_controller.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Scalar = tr.b.Scalar; + const Unit = tr.b.Unit; + const THIS_DOC = document.currentScript.ownerDocument; + + const EXAMPLE_MEMORY_FORMATTING_CONTEXT = { + unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI, + }; + const EXAMPLE_MEMORY_NUMERIC = new Scalar( + Unit.byName.sizeInBytesDelta_smallerIsBetter, 256 * 1024 * 1024); + + function checkSignificance(span, expectedSignificance) { + assert.strictEqual(span.$.insignificant.style.display, + expectedSignificance === 'insignificant' ? 'inline' : ''); + assert.strictEqual(span.$.significantly_better.style.display, + expectedSignificance === 'significantly_better' ? 'inline' : ''); + assert.strictEqual(span.$.significantly_worse.style.display, + expectedSignificance === 'significantly_worse' ? 'inline' : ''); + } + + function checkScalarSpan(test, value, unit, expectedContent, opt_options) { + const options = opt_options || {}; + const span = tr.v.ui.createScalarSpan(new tr.b.Scalar(unit, value), + {significance: options.significance}); + + test.addHTMLOutput(span); + assert.strictEqual( + Polymer.dom(span.$.content).textContent, expectedContent); + assert.strictEqual(window.getComputedStyle(span.$.content).color, + options.expectedColor || 'rgb(0, 0, 0)'); + + if (options.expectedTitle) { + assert.strictEqual(span.$.content.title, options.expectedTitle); + } + + if (options.significance !== undefined) { + checkSignificance(span, options.expectedEmoji); + if (options.expectedTitle) { + assert.strictEqual(span.$.significance.title, options.expectedTitle); + } + } + } + + test('instantiate_significance', function() { + const countD = Unit.byName.count.correspondingDeltaUnit; + const countSIBD = Unit.byName.count_smallerIsBetter.correspondingDeltaUnit; + const countBIBD = Unit.byName.count_biggerIsBetter.correspondingDeltaUnit; + + const zero = String.fromCharCode(177) + '0'; + + checkScalarSpan(this, 0, countSIBD, zero, { + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedTitle: 'no change', + expectedEmoji: '' + }); + + checkScalarSpan(this, 0, countSIBD, zero, { + expectedEmoji: 'insignificant', + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedEmojiColor: 'rgb(0, 0, 0)', + expectedTitle: 'no change' + }); + + assert.throws(() => checkScalarSpan(this, 0, countSIBD, zero, + {significance: tr.b.math.Statistics.Significance.SIGNIFICANT})); + + checkScalarSpan(this, 0, countBIBD, zero, { + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedTitle: 'no change', + expectedEmoji: '' + }); + + checkScalarSpan(this, 0, countBIBD, zero, { + expectedEmoji: 'insignificant', + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedEmojiColor: 'rgb(0, 0, 0)', + expectedTitle: 'no change' + }); + + assert.throws(() => checkScalarSpan(this, 0, countSIBD, zero, + {significance: tr.b.math.Statistics.Significance.SIGNIFICANT})); + + checkScalarSpan(this, 1, countSIBD, '+1', { + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedColor: 'rgb(255, 0, 0)', + expectedTitle: 'regression', + expectedEmoji: '' + }); + + checkScalarSpan(this, 1, countSIBD, '+1', { + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedColor: 'rgb(255, 0, 0)', + expectedEmoji: 'insignificant', + expectedEmojiColor: 'rgb(0, 0, 0)', + expectedTitle: 'insignificant regression' + }); + + checkScalarSpan(this, 1, countSIBD, '+1', { + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + expectedColor: 'rgb(255, 0, 0)', + expectedEmoji: 'significantly_worse', + expectedEmojiColor: 'rgb(255, 0, 0)', + expectedTitle: 'significant regression' + }); + + checkScalarSpan(this, 1, countBIBD, '+1', { + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedColor: 'rgb(0, 128, 0)', + expectedTitle: 'improvement', + expectedEmoji: '' + }); + + checkScalarSpan(this, 1, countBIBD, '+1', { + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedColor: 'rgb(0, 128, 0)', + expectedEmoji: 'insignificant', + expectedEmojiColor: 'rgb(0, 0, 0)', + expectedTitle: 'insignificant improvement' + }); + + checkScalarSpan(this, 1, countBIBD, '+1', { + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + expectedColor: 'rgb(0, 128, 0)', + expectedEmoji: 'significantly_better', + expectedEmojiColor: 'rgb(0, 128, 0)', + expectedTitle: 'significant improvement' + }); + + checkScalarSpan(this, -1, countSIBD, '-1', { + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedColor: 'rgb(0, 128, 0)', + expectedEmoji: '', + expectedEmojiColor: '', + expectedTitle: 'improvement' + }); + + checkScalarSpan(this, -1, countSIBD, '-1', { + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedColor: 'rgb(0, 128, 0)', + expectedEmoji: 'insignificant', + expectedEmojiColor: 'rgb(0, 0, 0)', + expectedTitle: 'insignificant improvement' + }); + + checkScalarSpan(this, -1, countSIBD, '-1', { + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + expectedColor: 'rgb(0, 128, 0)', + expectedEmoji: 'significantly_better', + expectedEmojiColor: 'rgb(0, 128, 0)', + expectedTitle: 'significant improvement' + }); + + checkScalarSpan(this, -1, countBIBD, '-1', { + expectedColor: 'rgb(255, 0, 0)', + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedEmoji: '' + }); + + checkScalarSpan(this, -1, countBIBD, '-1', { + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedColor: 'rgb(255, 0, 0)', + expectedEmoji: 'insignificant', + expectedEmojiColor: 'rgb(0, 0, 0)', + expectedTitle: 'insignificant regression' + }); + + checkScalarSpan(this, -1, countBIBD, '-1', { + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + expectedColor: 'rgb(255, 0, 0)', + expectedEmoji: 'significantly_worse', + expectedEmojiColor: 'rgb(255, 0, 0)', + expectedTitle: 'significant regression' + }); + + checkScalarSpan(this, 1, countD, '+1', { + expectedColor: 'rgb(0, 0, 0)', + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedEmoji: '' + }); + + checkScalarSpan(this, 1, countD, '+1', { + expectedColor: 'rgb(0, 0, 0)', + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedEmoji: '' + }); + + checkScalarSpan(this, 1, countD, '+1', { + expectedColor: 'rgb(0, 0, 0)', + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + expectedEmoji: '' + }); + + checkScalarSpan(this, -1, countD, '-1', { + expectedColor: 'rgb(0, 0, 0)', + significance: tr.b.math.Statistics.Significance.DONT_CARE, + expectedEmoji: '' + }); + + checkScalarSpan(this, -1, countD, '-1', { + expectedColor: 'rgb(0, 0, 0)', + significance: tr.b.math.Statistics.Significance.INSIGNIFICANT, + expectedEmoji: '' + }); + + checkScalarSpan(this, -1, countD, '-1', { + expectedColor: 'rgb(0, 0, 0)', + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + expectedEmoji: '' + }); + }); + + test('instantiate', function() { + checkScalarSpan(this, 123.456789, Unit.byName.timeDurationInMs, + '123.457 ms'); + checkScalarSpan(this, 0, Unit.byName.normalizedPercentage, '0.0%'); + checkScalarSpan(this, 1, Unit.byName.normalizedPercentage, '100.0%'); + checkScalarSpan(this, -2560, Unit.byName.sizeInBytes, '-2.5 KiB'); + }); + + test('instantiate_smallerIsBetter', function() { + checkScalarSpan(this, 45097156608, Unit.byName.sizeInBytes_smallerIsBetter, + '42.0 GiB'); + checkScalarSpan(this, 0, Unit.byName.energyInJoules_smallerIsBetter, + '0.000 J'); + checkScalarSpan(this, -0.25, Unit.byName.unitlessNumber_smallerIsBetter, + '-0.250'); + }); + + test('instantiate_biggerIsBetter', function() { + checkScalarSpan(this, 0.07, Unit.byName.powerInWatts_smallerIsBetter, + '70.000 mW'); + checkScalarSpan(this, 0, Unit.byName.timeStampInMs_biggerIsBetter, + '0.000 ms'); + checkScalarSpan(this, -0.003, + Unit.byName.normalizedPercentage_biggerIsBetter, '-0.3%'); + }); + + test('instantiate_delta', function() { + checkScalarSpan(this, 123.456789, Unit.byName.timeDurationInMsDelta, + '+123.457 ms'); + checkScalarSpan(this, 0, Unit.byName.normalizedPercentageDelta, + '\u00B10.0%'); + checkScalarSpan(this, -2560, Unit.byName.sizeInBytesDelta, + '-2.5 KiB'); + }); + + test('instantiate_delta_smallerIsBetter', function() { + checkScalarSpan(this, 45097156608, + Unit.byName.sizeInBytesDelta_smallerIsBetter, '+42.0 GiB', + {expectedColor: 'rgb(255, 0, 0)'}); + checkScalarSpan(this, 0, Unit.byName.energyInJoulesDelta_smallerIsBetter, + '\u00B10.000 J'); + checkScalarSpan(this, -0.25, + Unit.byName.unitlessNumberDelta_smallerIsBetter, '-0.250', + {expectedColor: 'rgb(0, 128, 0)'}); + }); + + test('instantiate_delta_biggerIsBetter', function() { + checkScalarSpan(this, 0.07, Unit.byName.powerInWattsDelta_biggerIsBetter, + '+70.000 mW', {expectedColor: 'rgb(0, 128, 0)'}); + checkScalarSpan(this, 0, Unit.byName.timeStampInMsDelta_biggerIsBetter, + '\u00B10.000 ms'); + checkScalarSpan(this, -0.003, + Unit.byName.normalizedPercentageDelta_biggerIsBetter, '-0.3%', + {expectedColor: 'rgb(255, 0, 0)'}); + }); + + test('createScalarSpan', function() { + // No config. + let span = tr.v.ui.createScalarSpan( + new Scalar(Unit.byName.powerInWatts, 3.14)); + assert.strictEqual(Polymer.dom(span.$.content).textContent, '3.140 W'); + assert.strictEqual(span.ownerDocument, document); + assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(span.value, 3.14); + assert.strictEqual(span.unit, Unit.byName.powerInWatts); + assert.isUndefined(span.context); + assert.isUndefined(span.customContextRange); + assert.isUndefined(span.warning); + assert.isFalse(span.leftAlign); + this.addHTMLOutput(span); + + // Inline. + const div = document.createElement('div'); + this.addHTMLOutput(div); + const inlineSpan = tr.v.ui.createScalarSpan( + new Scalar(Unit.byName.powerInWatts, 3.14), + {inline: true}); + assert.strictEqual(Polymer.dom(inlineSpan.$.content).textContent, + '3.140 W'); + assert.strictEqual(inlineSpan.ownerDocument, document); + assert.strictEqual(inlineSpan.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(inlineSpan.value, 3.14); + assert.strictEqual(inlineSpan.unit, Unit.byName.powerInWatts); + assert.isUndefined(inlineSpan.context); + assert.isUndefined(inlineSpan.customContextRange); + assert.isUndefined(inlineSpan.warning); + assert.isFalse(inlineSpan.leftAlign); + div.appendChild(document.createTextNode('prefix ')); + div.appendChild(inlineSpan); + div.appendChild(document.createTextNode(' suffix')); + assert.isBelow(inlineSpan.getBoundingClientRect().width, + span.getBoundingClientRect().width); + + // Custom owner document and right align. + span = tr.v.ui.createScalarSpan( + new Scalar(Unit.byName.energyInJoules, 2.72), + { ownerDocument: THIS_DOC, leftAlign: true}); + assert.strictEqual(Polymer.dom(span.$.content).textContent, '2.720 J'); + assert.strictEqual(span.ownerDocument, THIS_DOC); + assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(span.value, 2.72); + assert.strictEqual(span.unit, Unit.byName.energyInJoules); + assert.isUndefined(span.context); + assert.isUndefined(span.customContextRange); + assert.isUndefined(span.warning); + assert.isTrue(span.leftAlign); + this.addHTMLOutput(span); + + // Unit and sparkline set via config. + span = tr.v.ui.createScalarSpan(1.62, { + unit: Unit.byName.timeStampInMs, + customContextRange: tr.b.math.Range.fromExplicitRange(0, 3.24) + }); + assert.strictEqual(Polymer.dom(span.$.content).textContent, '1.620 ms'); + assert.strictEqual(span.ownerDocument, document); + assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(span.value, 1.62); + assert.strictEqual(span.unit, Unit.byName.timeStampInMs); + assert.isUndefined(span.context); + assert.isTrue(tr.b.math.Range.fromExplicitRange(0, 3.24).equals( + span.customContextRange)); + assert.isUndefined(span.warning); + assert.isFalse(span.leftAlign); + this.addHTMLOutput(span); + + // Custom context. + span = tr.v.ui.createScalarSpan( + new Scalar(Unit.byName.sizeInBytesDelta_smallerIsBetter, + 256 * 1024 * 1024), { context: { + unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI, + minimumFractionDigits: 2 + } }); + assert.strictEqual( + Polymer.dom(span.$.content).textContent, '+262,144.00 KiB'); + assert.strictEqual(span.ownerDocument, document); + assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN'); + assert.strictEqual(span.value, 256 * 1024 * 1024); + assert.strictEqual(span.unit, Unit.byName.sizeInBytesDelta_smallerIsBetter); + assert.deepEqual(span.context, { + unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI, + minimumFractionDigits: 2 + }); + assert.isUndefined(span.customContextRange); + assert.isUndefined(span.warning); + assert.isFalse(span.leftAlign); + this.addHTMLOutput(span); + }); + + test('instantiate_withWarning', function() { + const span = document.createElement('tr-v-ui-scalar-span'); + span.value = 400000000; + span.unit = Unit.byName.sizeInBytes; + span.warning = 'There is a problem with this size'; + this.addHTMLOutput(span); + }); + + test('instantiate_withCustomContextRange', function() { + const span = document.createElement('tr-v-ui-scalar-span'); + span.value = new Scalar(Unit.byName.unitlessNumber, 0.99); + span.customContextRange = tr.b.math.Range.fromExplicitRange(0, 3); + this.addHTMLOutput(span); + }); + + test('instantiate_withRightAlign', function() { + const span = document.createElement('tr-v-ui-scalar-span'); + span.value = new Scalar(Unit.byName.timeStampInMs, 5.777); + span.leftAlign = true; + this.addHTMLOutput(span); + }); + + test('instantiate_withContext', function() { + const span = document.createElement('tr-v-ui-scalar-span'); + span.value = new Scalar( + Unit.byName.unitlessNumberDelta_smallerIsBetter, 42); + span.context = { maximumFractionDigits: 2 }; + assert.strictEqual(Polymer.dom(span.$.content).textContent, '+42.00'); + this.addHTMLOutput(span); + }); + + test('deltaAndNonDeltaHaveSimilarHeights', function() { + const spanA = document.createElement('tr-v-ui-scalar-span'); + spanA.setValueAndUnit(400, Unit.byName.timeDurationInMs); + checkSignificance(spanA, ''); + + const spanB = document.createElement('tr-v-ui-scalar-span'); + spanB.setValueAndUnit(400, Unit.byName.timeDurationInMsDelta); + checkSignificance(spanB, ''); + + const spanC = document.createElement('tr-v-ui-scalar-span'); + spanC.setValueAndUnit( + 400, Unit.byName.timeDurationInMsDelta_smallerIsBetter); + spanC.significance = tr.b.math.Statistics.Significance.SIGNIFICANT; + checkSignificance(spanC, 'significantly_worse'); + + const spanD = document.createElement('tr-v-ui-scalar-span'); + spanD.setValueAndUnit( + 400, Unit.byName.timeDurationInMsDelta_biggerIsBetter); + spanD.significance = tr.b.math.Statistics.Significance.SIGNIFICANT; + checkSignificance(spanD, 'significantly_better'); + + const spanE = document.createElement('tr-v-ui-scalar-span'); + spanE.setValueAndUnit( + 400, Unit.byName.timeDurationInMsDelta_smallerIsBetter); + spanE.significance = tr.b.math.Statistics.Significance.INSIGNIFICANT; + checkSignificance(spanE, 'insignificant'); + + const overall = document.createElement('div'); + overall.style.display = 'flex'; + // These spans must be on separate lines so that Chrome has the option of + // making their heights different. The point of the test is that Chrome + // shouldn't have to make their heights different even when it could. + overall.style.flexDirection = 'column'; + Polymer.dom(overall).appendChild(spanA); + Polymer.dom(overall).appendChild(spanB); + Polymer.dom(overall).appendChild(spanC); + Polymer.dom(overall).appendChild(spanD); + Polymer.dom(overall).appendChild(spanE); + this.addHTMLOutput(overall); + + const expectedHeight = spanA.getBoundingClientRect().height; + assert.strictEqual(expectedHeight, spanB.getBoundingClientRect().height); + assert.strictEqual(expectedHeight, spanC.getBoundingClientRect().height); + assert.strictEqual(expectedHeight, spanD.getBoundingClientRect().height); + assert.strictEqual(expectedHeight, spanE.getBoundingClientRect().height); + }); + + test('warningAndNonWarningHaveSimilarHeights', function() { + const spanA = document.createElement('tr-v-ui-scalar-span'); + spanA.setValueAndUnit(400, Unit.byName.timeDurationInMs); + + const spanB = document.createElement('tr-v-ui-scalar-span'); + spanB.setValueAndUnit(400, Unit.byName.timeDurationInMs); + spanB.warning = 'There is a problem with this time'; + + const overall = document.createElement('div'); + overall.style.display = 'flex'; + // These spans must be on separate lines so that Chrome has the option of + // making their heights different. The point of the test is that Chrome + // shouldn't have to make their heights different even when it could. + overall.style.flexDirection = 'column'; + Polymer.dom(overall).appendChild(spanA); + Polymer.dom(overall).appendChild(spanB); + this.addHTMLOutput(overall); + + const rectA = spanA.getBoundingClientRect(); + const rectB = spanB.getBoundingClientRect(); + assert.strictEqual(rectA.height, rectB.height); + }); + + test('respectCurrentDisplayUnit', function() { + try { + Unit.currentTimeDisplayMode = tr.b.TimeDisplayModes.ns; + + const span = document.createElement('tr-v-ui-scalar-span'); + span.setValueAndUnit(73, Unit.byName.timeStampInMs); + this.addHTMLOutput(span); + + assert.isTrue(Polymer.dom(span.$.content).textContent.indexOf('ns') > 0); + Unit.currentTimeDisplayMode = tr.b.TimeDisplayModes.ms; + assert.isTrue(Polymer.dom(span.$.content).textContent.indexOf('ms') > 0); + } finally { + Unit.reset(); + } + }); + + function checkSparkline(span, expectation) { + tr.b.forceAllPendingTasksToRunForTest(); + const sparklineEl = span.$.sparkline; + const computedStyle = getComputedStyle(sparklineEl); + + const expectedDisplay = expectation.display || 'block'; + assert.strictEqual(computedStyle.display, expectedDisplay); + if (expectedDisplay === 'none') { + // Test expectation sanity check. + assert.notProperty(expectation, 'left'); + assert.notProperty(expectation, 'width'); + assert.notProperty(expectation, 'classList'); + return; + } + + assert.closeTo(parseFloat(computedStyle.left), expectation.left, 0.1); + assert.closeTo(parseFloat(computedStyle.width), expectation.width, 0.1); + assert.sameMembers(Array.from(sparklineEl.classList), + expectation.classList || []); + } + + test('customContextRange', function() { + const div = document.createElement('div'); + div.style.width = '101px'; // One extra pixel for sparkline border. + this.addHTMLOutput(div); + + // No custom context range. + const span1 = tr.v.ui.createScalarSpan(0, { + unit: Unit.byName.timeStampInMs + }); + Polymer.dom(div).appendChild(span1); + checkSparkline(span1, {display: 'none'}); + const span2 = tr.v.ui.createScalarSpan(0, { + unit: Unit.byName.timeStampInMs, + customContextRange: undefined + }); + Polymer.dom(div).appendChild(span2); + checkSparkline(span2, {display: 'none'}); + const span3 = tr.v.ui.createScalarSpan(0, { + unit: Unit.byName.timeStampInMs, + customContextRange: new tr.b.math.Range() // Empty range. + }); + Polymer.dom(div).appendChild(span3); + checkSparkline(span3, {display: 'none'}); + + const range = tr.b.math.Range.fromExplicitRange(-15, 15); + + // Values inside custom context range. + const span4 = tr.v.ui.createScalarSpan(-15, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span4); + checkSparkline(span4, {left: 0, width: 51}); + const span5 = tr.v.ui.createScalarSpan(-14, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span5); + checkSparkline(span5, {left: 3.33, width: 47.67}); + const span6 = tr.v.ui.createScalarSpan(-10, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span6); + checkSparkline(span6, {left: 16.67, width: 34.33}); + const span7 = tr.v.ui.createScalarSpan(0, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span7); + checkSparkline(span7, {left: 50, width: 1}); + const span8 = tr.v.ui.createScalarSpan(10, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span8); + checkSparkline(span8, {left: 50, width: 34.33, classList: ['positive']}); + const span9 = tr.v.ui.createScalarSpan(14, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span9); + checkSparkline(span9, {left: 50, width: 47.67, classList: ['positive']}); + const span10 = tr.v.ui.createScalarSpan(15, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span10); + checkSparkline(span10, {left: 50, width: 51, classList: ['positive']}); + + // Values outside custom context range. + const span11 = tr.v.ui.createScalarSpan(-20, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span11); + checkSparkline(span11, {left: 0, width: 51}); + const span12 = tr.v.ui.createScalarSpan(20, { + unit: Unit.byName.timeStampInMs, + customContextRange: range + }); + Polymer.dom(div).appendChild(span12); + checkSparkline(span12, {left: 50, width: 51, classList: ['positive']}); + }); + + test('emptyNumeric', function() { + assert.strictEqual(tr.v.ui.createScalarSpan(), ''); + }); + + test('contextControllerChanges', function() { + const div = document.createElement('div'); + div.style.width = '101px'; // One extra pixel for sparkline border. + this.addHTMLOutput(div); + + div.appendChild( + document.createElement('tr-v-ui-scalar-context-controller')); + + const s1 = tr.v.ui.createScalarSpan(10, { + unit: Unit.byName.powerInWatts + }); + Polymer.dom(div).appendChild(s1); + checkSparkline(s1, {display: 'none'}); + + const s2 = tr.v.ui.createScalarSpan(20, { + unit: Unit.byName.powerInWatts, + contextGroup: 'A' + }); + Polymer.dom(div).appendChild(s2); + checkSparkline(s1, {display: 'none'}); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + + const s3 = tr.v.ui.createScalarSpan(30, { + unit: Unit.byName.powerInWatts, + contextGroup: 'A' + }); + Polymer.dom(div).appendChild(s3); + checkSparkline(s1, {display: 'none'}); + checkSparkline(s2, {left: 0, width: 67.67, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + + const s4 = tr.v.ui.createScalarSpan(40, { + unit: Unit.byName.powerInWatts, + contextGroup: 'B' + }); + Polymer.dom(div).appendChild(s4); + checkSparkline(s1, {display: 'none'}); + checkSparkline(s2, {left: 0, width: 67.67, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {left: 0, width: 101, classList: ['positive']}); + + s3.contextGroup = 'B'; + checkSparkline(s1, {display: 'none'}); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 76, classList: ['positive']}); + checkSparkline(s4, {left: 0, width: 101, classList: ['positive']}); + + s1.setAttribute('context-group', 'A'); + checkSparkline(s1, {left: 0, width: 51, classList: ['positive']}); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 76, classList: ['positive']}); + checkSparkline(s4, {left: 0, width: 101, classList: ['positive']}); + + s1.value = 50; + checkSparkline(s1, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s2, {left: 0, width: 41, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 76, classList: ['positive']}); + checkSparkline(s4, {left: 0, width: 101, classList: ['positive']}); + + s1.customContextRange = tr.b.math.Range.fromExplicitRange(0, 150); + checkSparkline(s1, {left: 0, width: 34.33, classList: ['positive']}); + checkSparkline(s2, {left: 0, width: 41, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 76, classList: ['positive']}); + checkSparkline(s4, {left: 0, width: 101, classList: ['positive']}); + + s4.contextGroup = null; + checkSparkline(s1, {left: 0, width: 34.33, classList: ['positive']}); + checkSparkline(s2, {left: 0, width: 41, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + + s1.customContextRange = undefined; + checkSparkline(s1, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s2, {left: 0, width: 41, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + + s4.value = 0; + checkSparkline(s1, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s2, {left: 0, width: 41, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + + div.removeChild(s1); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + + s1.contextGroup = 'B'; + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + + div.appendChild(s1); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 61, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + checkSparkline(s1, {left: 0, width: 101, classList: ['positive']}); + + s1.removeAttribute('context-group'); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + checkSparkline(s1, {display: 'none'}); + + s1.customContextRange = tr.b.math.Range.fromExplicitRange(0, 100); + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s4, {display: 'none'}); + checkSparkline(s1, {left: 0, width: 51, classList: ['positive']}); + + s3.value = 0; + checkSparkline(s2, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(s3, {display: 'none'}); + checkSparkline(s4, {display: 'none'}); + checkSparkline(s1, {left: 0, width: 51, classList: ['positive']}); + }); + + test('deltaSparkline_noImprovementDirection', function() { + const div = document.createElement('div'); + div.style.width = '101px'; // One extra pixel for sparkline border. + this.addHTMLOutput(div); + div.appendChild( + document.createElement('tr-v-ui-scalar-context-controller')); + + const span1 = tr.v.ui.createScalarSpan(20971520, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span1); + const span2 = tr.v.ui.createScalarSpan(15728640, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span2); + const span3 = tr.v.ui.createScalarSpan(12582912, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span3); + const span4 = tr.v.ui.createScalarSpan(11534336, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span4); + const span5 = tr.v.ui.createScalarSpan(10485760, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span5); + const span6 = tr.v.ui.createScalarSpan(9437184, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span6); + const span7 = tr.v.ui.createScalarSpan(8388608, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span7); + const span8 = tr.v.ui.createScalarSpan(5242880, { + unit: Unit.byName.sizeInBytesDelta, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span8); + + // We must check the sparklines *after* all spans are appended because new + // values can change the context range. + checkSparkline(span1, {left: 0, width: 101, classList: ['positive']}); + checkSparkline(span2, {left: 0, width: 76, classList: ['positive']}); + checkSparkline(span3, {left: 0, width: 61, classList: ['positive']}); + checkSparkline(span4, {left: 0, width: 56, classList: ['positive']}); + checkSparkline(span5, {left: 0, width: 51, classList: ['positive']}); + checkSparkline(span6, {left: 0, width: 46, classList: ['positive']}); + checkSparkline(span7, {left: 0, width: 41, classList: ['positive']}); + checkSparkline(span8, {left: 0, width: 26, classList: ['positive']}); + }); + + test('deltaSparkline_smallerIsBetter', function() { + const div = document.createElement('div'); + div.style.width = '101px'; // One extra pixel for sparkline border. + this.addHTMLOutput(div); + div.appendChild( + document.createElement('tr-v-ui-scalar-context-controller')); + + const span1 = tr.v.ui.createScalarSpan(5242880, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span1); + const span2 = tr.v.ui.createScalarSpan(0, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span2); + const span3 = tr.v.ui.createScalarSpan(-3145728, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span3); + const span4 = tr.v.ui.createScalarSpan(-4194304, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span4); + const span5 = tr.v.ui.createScalarSpan(-5242880, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span5); + const span6 = tr.v.ui.createScalarSpan(-6291456, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span6); + const span7 = tr.v.ui.createScalarSpan(-7340032, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span7); + const span8 = tr.v.ui.createScalarSpan(-15728640, { + unit: Unit.byName.sizeInBytesDelta_smallerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span8); + + // We must check the sparklines *after* all spans are appended because new + // values can change the context range. + checkSparkline(span1, + {left: 75, width: 26, classList: ['positive', 'worse']}); + checkSparkline(span2, {left: 75, width: 1, classList: ['same']}); + checkSparkline(span3, {left: 60, width: 16, classList: ['better']}); + checkSparkline(span4, {left: 55, width: 21, classList: ['better']}); + checkSparkline(span5, {left: 50, width: 26, classList: ['better']}); + checkSparkline(span6, {left: 45, width: 31, classList: ['better']}); + checkSparkline(span7, {left: 40, width: 36, classList: ['better']}); + checkSparkline(span8, {left: 0, width: 76, classList: ['better']}); + }); + + test('deltaSparkline_biggerIsBetter', function() { + const div = document.createElement('div'); + div.style.width = '101px'; // One extra pixel for sparkline border. + this.addHTMLOutput(div); + div.appendChild( + document.createElement('tr-v-ui-scalar-context-controller')); + + const span1 = tr.v.ui.createScalarSpan(0, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span1); + const span2 = tr.v.ui.createScalarSpan(-5242880, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span2); + const span3 = tr.v.ui.createScalarSpan(-8388608, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span3); + const span4 = tr.v.ui.createScalarSpan(-9437184, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span4); + const span5 = tr.v.ui.createScalarSpan(-10485760, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span5); + const span6 = tr.v.ui.createScalarSpan(-11534336, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span6); + const span7 = tr.v.ui.createScalarSpan(-12582912, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span7); + const span8 = tr.v.ui.createScalarSpan(-20971520, { + unit: Unit.byName.sizeInBytesDelta_biggerIsBetter, + contextGroup: 'test' + }); + Polymer.dom(div).appendChild(span8); + + // We must check the sparklines *after* all spans are appended because new + // values can change the context range. + checkSparkline(span1, {left: 100, width: 1, classList: ['same']}); + checkSparkline(span2, {left: 75, width: 26, classList: ['worse']}); + checkSparkline(span3, {left: 60, width: 41, classList: ['worse']}); + checkSparkline(span4, {left: 55, width: 46, classList: ['worse']}); + checkSparkline(span5, {left: 50, width: 51, classList: ['worse']}); + checkSparkline(span6, {left: 45, width: 56, classList: ['worse']}); + checkSparkline(span7, {left: 40, width: 61, classList: ['worse']}); + checkSparkline(span8, {left: 0, width: 101, classList: ['worse']}); + }); + + test('classListChanges', function() { + const div = document.createElement('div'); + div.style.width = '200px'; + this.addHTMLOutput(div); + + const span = tr.v.ui.createScalarSpan(10, { + unit: Unit.byName.energyInJoulesDelta_smallerIsBetter, + significance: tr.b.math.Statistics.Significance.SIGNIFICANT, + customContextRange: tr.b.math.Range.fromExplicitRange(-20, 20) + }); + Polymer.dom(div).appendChild(span); + + assert.sameMembers(Array.from(span.$.content.classList), ['worse']); + checkSignificance(span, 'significantly_worse'); + + span.significance = tr.b.math.Statistics.Significance.DONT_CARE; + assert.sameMembers(Array.from(span.$.sparkline.classList), + ['positive', 'worse']); + assert.sameMembers(Array.from(span.$.content.classList), ['worse']); + checkSignificance(span, ''); + + span.value = -5; + assert.sameMembers(Array.from(span.$.sparkline.classList), ['better']); + assert.sameMembers(Array.from(span.$.content.classList), ['better']); + checkSignificance(span, ''); + + span.unit = Unit.byName.energyInJoules; + assert.sameMembers(Array.from(span.$.sparkline.classList), []); + assert.sameMembers(Array.from(span.$.content.classList), []); + checkSignificance(span, ''); + + span.value = 20; + assert.sameMembers(Array.from(span.$.sparkline.classList), ['positive']); + assert.sameMembers(Array.from(span.$.content.classList), []); + checkSignificance(span, ''); + + span.unit = Unit.byName.energyInJoulesDelta_biggerIsBetter; + assert.sameMembers(Array.from(span.$.sparkline.classList), + ['positive', 'better']); + assert.sameMembers(Array.from(span.$.content.classList), ['better']); + checkSignificance(span, ''); + + span.significance = tr.b.math.Statistics.Significance.INSIGNIFICANT; + assert.sameMembers(Array.from(span.$.sparkline.classList), + ['positive', 'better']); + assert.sameMembers(Array.from(span.$.content.classList), ['better']); + checkSignificance(span, 'insignificant'); + + span.unit = Unit.byName.energyInJoulesDelta_smallerIsBetter; + assert.sameMembers(Array.from(span.$.sparkline.classList), + ['positive', 'worse']); + assert.sameMembers(Array.from(span.$.content.classList), ['worse']); + checkSignificance(span, 'insignificant'); + + span.unit = Unit.byName.energyInJoulesDelta; + assert.sameMembers(Array.from(span.$.sparkline.classList), ['positive']); + assert.sameMembers(Array.from(span.$.content.classList), []); + checkSignificance(span, ''); + + span.value = 0; + assert.sameMembers(Array.from(span.$.sparkline.classList), []); + assert.sameMembers(Array.from(span.$.content.classList), []); + checkSignificance(span, ''); + }); + + test('sparkline_uncentered', function() { + const div = document.createElement('div'); + this.addHTMLOutput(div); + div.appendChild( + document.createElement('tr-v-ui-scalar-context-controller')); + + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(-1, { + unit: Unit.byName.powerInWattsDelta, + contextGroup: 'test' + })); + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(100, { + unit: Unit.byName.powerInWattsDelta, + contextGroup: 'test' + })); + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(80, { + unit: Unit.byName.powerInWattsDelta, + contextGroup: 'test' + })); + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(60, { + unit: Unit.byName.powerInWattsDelta, + contextGroup: 'test' + })); + }); + + test('sparkline_centered', function() { + const div = document.createElement('div'); + this.addHTMLOutput(div); + div.appendChild( + document.createElement('tr-v-ui-scalar-context-controller')); + + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(-1, { + unit: Unit.byName.powerInWattsDelta, + customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100) + })); + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(100, { + unit: Unit.byName.powerInWattsDelta, + customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100) + })); + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(80, { + unit: Unit.byName.powerInWattsDelta, + customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100) + })); + Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(60, { + unit: Unit.byName.powerInWattsDelta, + customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100) + })); + }); + + timedPerfTest('memory_scalar_spans', function() { + tr.v.ui.createScalarSpan(EXAMPLE_MEMORY_NUMERIC, { + context: EXAMPLE_MEMORY_FORMATTING_CONTEXT, + inline: true, + }); + }, { + iterations: 1000, + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/timings.md b/chromium/third_party/catapult/tracing/tracing/value/ui/timings.md new file mode 100644 index 00000000000..3846af6ee0d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/timings.md @@ -0,0 +1,78 @@ +This document describes the Google Analytics metrics reported by results.html. + +Measures are recorded by the performance web API, and are visible in the User +Timing track in the devtools Performance timeline. + +Instant events are recorded using console.timestamp(), and are visible as orange +ticks at the top of the devtools Performance timeline. + +Both measures and instant events are recorded as Events in Google Analytics. +[Access these metrics here](https://analytics.google.com/analytics/web/#embed/report-home/a98760012w145165698p149871853/) if you have been granted access. + + * histogram-set-controls + * `alpha` measures response latency of changing the statistical significance + threshold, alpha. + * `hideOverviewCharts` measures response latency of hiding all overview + charts. + * `referenceColumn` measures response latency of changing the reference + column. + * `search` measures response latency of changing the search query to filter + rows. + * `showAll` measures response latency of toggling showing all rows versus + source Histograms only. + * `showOverviewCharts` measures response latency of showing all overview + charts. + * `statistic` measures response latency of changing the statistic that is + displayed in histogram-set-table-cells. + * HistogramSetLocation + * `onPopState` measures response latency of the browser back button. + * `pushState` measures latency of serializing the view state and pushing it + to the HTML5 history API. This happens automatically whenever any part of + the ViewState is updated. + * histogram-set-table + * `columnCount` instant event contains the number of columns, recorded when the + table is built. + * `nameColumnConstrained` instant event recorded when the name column width + is constrained. + * `nameColumnUnconstrained` instant event recorded when the name column width + is unconstrained. + * `rootRowCount` instant event contains the number of root rows, recorded + whenever it changes or the table is built. + * `rowCollapsed` instant event recorded whenever a row is collapsed. + * `rowExpanded` instant event recorded whenever a row is expanded. + * `selectHistogramNames` instant event recorded whenever a breakdown related + histogram name link is clicked. + * `sortColumn` instant event recorded whenever the user changes the sort + column. + * histogram-set-table-cell + * `close` instant event recorded when the cell is closed. + * `open` instant event recorded when the cell is opened. + * histogram-set-table-name-cell + * `closeHistograms` instant event recorded when the user clicks the button to + close all histogram-set-table-cells in the row. + * `hideOverview` instant event recorded when the user clicks the button to + hide the overview line charts for the row. + * `openHistograms` instant event recorded when the user clicks the button to + open all histogram-set-table-cells in the row. + * `showOverview` instant event recorded when the user clicks the button to + show the overview line charts for the row. + * histogram-set-view + * `build` measures latency to find source Histograms, collect parameters, + configure the controls and build the table. Does not include parsing + Histograms from json. + * `sourceHistograms` measures latency to find source Histograms in the + relationship graphical model. + * `collectParameters` measures latency to collect display labels, statistic + names, and possible groupings. + * `export{Raw,Merged}{CSV,JSON}` measures latency to download a CSV/JSON file + of raw/merged Histograms. + * histogram-span + * `brushBins` instant event recorded when the user finishes brushing bins. + * `clearBrushedBins` instant event recorded when the user clears brushed + bins. + * `mergeSampleDiagnostics` measures latency of displaying the table of merged + sample diagnostics. + * `splitSampleDiagnostics` measures latency of displaying the table of + unmerged sample diagnostics. + * HistogramParameterCollector + * `maxSampleCount` instant event records maximum Histogram.numValues diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html new file mode 100644 index 00000000000..de68e0cfa57 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<!-- + This file only depends on diagnostic_span.html, but it must be imported from + diagnostic_span.html. + Fortunately, this file is only imported from diagnostic_span.html, so it can + just not import anything. +--> + +<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html"> + +<dom-module id="tr-v-ui-unmergeable-diagnostic-set-span"> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.v.ui', function() { + Polymer({ + is: 'tr-v-ui-unmergeable-diagnostic-set-span', + behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR], + + updateContents_() { + Polymer.dom(this).textContent = ''; + for (const diagnostic of this.diagnostic) { + if (diagnostic instanceof tr.v.d.RelatedNameMap) continue; + const div = document.createElement('div'); + div.appendChild(tr.v.ui.createDiagnosticSpan( + diagnostic, this.name_, this.histogram_)); + Polymer.dom(this).appendChild(div); + } + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html new file mode 100644 index 00000000000..29e48966fc0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html"> +<link rel="import" href="/tracing/value/ui/diagnostic_span.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const event = tr.c.TestUtils.newSliceEx({ + title: 'event', + start: 0, + duration: 1, + }); + event.parentContainer = { + sliceGroup: { + stableId: 'fake_thread', + slices: [event], + }, + }; + const diagnostics = new tr.v.d.UnmergeableDiagnosticSet([ + new tr.v.d.GenericSet(['generic diagnostic']), + new tr.v.d.RelatedNameMap(), + new tr.v.d.RelatedEventSet([ + event, + ]), + ]); + const span = tr.v.ui.createDiagnosticSpan(diagnostics); + assert.strictEqual('TR-V-UI-UNMERGEABLE-DIAGNOSTIC-SET-SPAN', span.tagName); + this.addHTMLOutput(span); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html new file mode 100644 index 00000000000..0adeaf55e3c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html @@ -0,0 +1,410 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2018 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/value/ui/metrics_visualization.html"> +<link rel="import" href="/tracing/value/ui/raster_visualization.html"> +<meta charset="utf-8"> +<dom-module id='tr-v-ui-visualizations-data-container'> + <template> + <style> + .error { + color: red; + display: none; + } + + .sample{ + display: none; + } + + .subtitle{ + font-size: 20px; + font-weight: bold; + padding-bottom: 5px; + } + + .description{ + font-size: 15px; + padding-bottom: 5px; + } + + #title { + font-size: 30px; + font-weight: bold; + padding-bottom: 5px; + } + </style> + <div id="title">Visualizations</div> + <div id="data_error" class="error">Invalid data provided.</div> + <div id="pipeline_per_frame_container"> + <div class="subtitle">Graphics Pipeline and Raster Tasks</div> + <div class="description"> + When raster tasks are completed in comparison to the rest of the graphics pipeline.<br> + Only pages where raster tasks are completed after beginFrame is issued are included. + </div> + <tr-v-ui-raster-visualization id="rasterVisualization"> + </tr-v-ui-raster-visualization> + </div> + <div id=metrics_container> + <div class="subtitle">Metrics</div> + <div class="description">Total amount of time taken for the indicated metrics.</div> + <tr-v-ui-metrics-visualization id="metricsVisualization" class="sample"> + </tr-v-ui-metrics-visualization> + </div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.v.ui', function() { + const STATISTICS_KEY = 'statistics'; + const SUBMETRICS_KEY = 'submetrics'; + const AGGREGATE_KEY = 'aggregate'; + const RASTER_START_METRIC_KEY = 'pipeline:begin_frame_to_raster_start'; + + const COLORS = [ + ['#FFD740', '#FFC400', '#FFAB00', '#E29800'], + ['#FF6E40', '#FF3D00', '#DD2C00', '#A32000'], + ['#40C4FF', '#00B0FF', '#0091EA', '#006DAF'], + ['#89C641', '#54B503', '#4AA510', '#377A0D'], + ['#B388FF', '#7C4DFF', '#651FFF', '#6200EA'], + ['#FF80AB', '#FF4081', '#F50057', '#C51162'], + ['#FFAB40', '#FF9100', '#FF6D00', '#D65C02'], + ['#8C9EFF', '#536DFE', '#3D5AFE', '#304FFE']]; + + const FRAME = [new Map([ + ['pipeline:begin_frame_to_raster_start', false], + ['pipeline:begin_frame_to_raster_end', true]]), new Map([ + ['pipeline:begin_frame_transport', true], + ['pipeline:begin_frame_to_frame_submission', true], + ['pipeline:frame_submission_to_display', true], + ['pipeline:draw', true]])]; + + const METRICS = new Map([ + ['Pipeline', [ + 'pipeline:begin_frame_transport', + 'pipeline:begin_frame_to_frame_submission', + 'pipeline:frame_submission_to_display', + 'pipeline:draw']], + ['Thread', [ + 'thread_browser_cpu_time_per_frame', + 'thread_display_compositor_cpu_time_per_frame', + 'thread_GPU_cpu_time_per_frame', + 'thread_IO_cpu_time_per_frame', + 'thread_other_cpu_time_per_frame', + 'thread_raster_cpu_time_per_frame', + 'thread_renderer_compositor_cpu_time_per_frame', + 'thread_renderer_main_cpu_time_per_frame']]]); + + function getValueFromMap(key, map) { + let retrievedValue = map.get(key); + if (!retrievedValue) { + retrievedValue = new Map(); + map.set(key, retrievedValue); + } + return retrievedValue; + } + + Polymer({ + is: 'tr-v-ui-visualizations-data-container', + + created() { + // from earliest to latest + this.orderedBenchmarks_ = []; + // aggregate/page -> benchmark -> metric -> statistics/submetrics + this.groupedData_ = new Map(); + }, + + build(leafHistograms, histograms) { + if (!leafHistograms || leafHistograms.length < 1 || + !histograms || histograms.length < 1) { + this.$.data_error.style.display = 'block'; + return; + } + + this.processHistograms_(this.groupHistograms_(histograms), + this.groupHistograms_(leafHistograms)); + this.buildCharts_(); + }, + + processHistograms_(histograms, leafHistograms) { + const benchmarkStartGrouping = tr.v.HistogramGrouping.BY_KEY.get( + tr.v.d.RESERVED_NAMES.BENCHMARK_START); + + const benchmarkToStartTime = new Map(); + for (const [metric, benchmarks] of histograms.entries()) { + // process aggregate data + for (const [benchmark, pages] of leafHistograms.get(metric).entries()) { + for (const [page, histograms] of pages.entries()) { + for (const histogram of histograms) { + const aggregateToBenchmarkMap = getValueFromMap(AGGREGATE_KEY, + this.groupedData_); + const benchmarkToMetricMap = getValueFromMap(benchmark, + aggregateToBenchmarkMap); + + benchmarkToMetricMap.set(metric, new Map([ + [STATISTICS_KEY, histogram.running]])); + } + } + } + + // process data per page + for (const [benchmark, pages] of benchmarks.entries()) { + for (const [page, histograms] of pages.entries()) { + for (const histogram of histograms) { + if (!benchmarkToStartTime.get(benchmark)) { + benchmarkToStartTime.set(benchmark, + benchmarkStartGrouping.callback(histogram)); + } + + const pageToBenchmarkMap = getValueFromMap(page, + this.groupedData_); + const benchmarkToMetricMap = getValueFromMap(benchmark, + pageToBenchmarkMap); + + // retrieving submetric _ta + const mergedSubmetrics = new tr.v.d.DiagnosticMap(); + for (const bin of histogram.allBins) { + for (const map of bin.diagnosticMaps) { + mergedSubmetrics.addDiagnostics(map); + } + } + + if (benchmarkToMetricMap.get(metric)) continue; + benchmarkToMetricMap.set(metric, new Map([ + [STATISTICS_KEY, histogram.running], + [SUBMETRICS_KEY, mergedSubmetrics.get('breakdown')]])); + } + } + } + } + this.orderedBenchmarks_ = this.sortBenchmarks_(benchmarkToStartTime); + }, + + groupHistograms_(histograms) { + const groupings = [ + tr.v.HistogramGrouping.HISTOGRAM_NAME, + tr.v.HistogramGrouping.DISPLAY_LABEL, + tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES)]; + + return histograms.groupHistogramsRecursively(groupings); + }, + + sortBenchmarks_(benchmarks) { + return Array.from(benchmarks.keys()).sort((a, b) => { + Date.parse(benchmarks.get(a)) - Date.parse(benchmarks.get(b)); + }); + }, + + getSeriesKey_(metric, benchmark) { + return metric + '-' + benchmark; + }, + + buildCharts_() { + const rasterDataToBePassed = this.buildRasterChart_(); + this.$.rasterVisualization.build(rasterDataToBePassed); + + for (const chartName of METRICS.keys()) { + const metricsDataToBePassed = this.buildMetricsData_(chartName); + const newChart = this.$.metricsVisualization.cloneNode(true); + newChart.style.display = 'block'; + Polymer.dom(this.$.metrics_container).appendChild(newChart); + newChart.build(metricsDataToBePassed); + } + }, + + buildRasterChart_() { + const orderedPages = [...this.groupedData_.keys()] + .filter((page) => this.filterPagesWithoutRasterMetric_(page)) + .sort((a, b) => this.sortByRasterStart_(a, b)); + const allChartData = new Map(); + for (const page of orderedPages) { + const pageMap = this.groupedData_.get(page); + let chartData = []; + for (const benchmark of this.orderedBenchmarks_) { + if (!pageMap.has(benchmark)) continue; + const benchmarkMap = pageMap.get(benchmark); + const benchmarkData = []; + if (benchmarkMap.get(RASTER_START_METRIC_KEY) === undefined) { + continue; + } + for (const [threadName, thread] of FRAME.entries()) { + const data = {x: benchmark, hide: 0}; + if (page !== AGGREGATE_KEY) data.group = page; + let rasterBegin = 0; + for (const metric of thread.keys()) { + const metricMap = benchmarkMap.get(metric); + const key = this.getSeriesKey_(metric, + data.x + '-' + threadName); + const stats = metricMap.get(STATISTICS_KEY); + const mean = stats ? stats.mean : 0; + let roundedMean = Math.round(mean * 100) / 100; + if (metric === RASTER_START_METRIC_KEY) { + rasterBegin = roundedMean; + } else if (metric === 'pipeline:begin_frame_to_raster_end') { + roundedMean -= rasterBegin; + } + data[key] = roundedMean; + } + benchmarkData.push(data); + } + chartData = chartData.concat(benchmarkData); + } + allChartData.set(page, chartData); + } + return allChartData; + }, + + buildMetricsData_(chartName) { + // pages are ordered from smallest to largest by their total + // values for the first benchmark + const orderedPages = [...this.groupedData_.keys()].sort((a, b) => + this.sortByTotal_(a, b, chartName)); + const chartData = []; + const aggregateChart = []; + for (const page of orderedPages) { + const pageMap = this.groupedData_.get(page); + for (const benchmark of this.orderedBenchmarks_) { + if (!pageMap.has(benchmark)) continue; + const data = {x: benchmark, group: page}; + const benchmarkMap = pageMap.get(benchmark); + for (const metric of METRICS.get(chartName)) { + const metricMap = benchmarkMap.get(metric); + const key = this.getSeriesKey_(metric, benchmark); + const stats = metricMap.get(STATISTICS_KEY); + const mean = stats ? stats.mean : 0; + data[key] = Math.round(mean * 100) / 100; + } + if (page === AGGREGATE_KEY) { + aggregateChart.push(data); + } else { + chartData.push(data); + } + } + chartData.push({}); + } + chartData.shift(); + return { + title: chartName, + aggregate: aggregateChart, + page: chartData, + submetrics: this.processSubmetricsData_(chartName) + }; + }, + + submetricsHelper_(submetric, value, benchmark, metricToSubmetricMap) { + let submetricToBenchmarkMap = metricToSubmetricMap.get(submetric); + if (!submetricToBenchmarkMap) { + submetricToBenchmarkMap = []; + metricToSubmetricMap.set(submetric, submetricToBenchmarkMap); + } + const data = {x: submetric, hide: 0, group: benchmark}; + const mean = value; + const roundedMean = Math.round(mean * 100) / 100; + if (!roundedMean) return; + data[this.getSeriesKey_(submetric, benchmark)] = roundedMean; + submetricToBenchmarkMap.push(data); + }, + + // Get data for breakdown of a main step + processSubmetricsData_(chartName) { + // page -> metric -> submetric -> + // array of submetrics across all benchmarks + const submetrics = new Map(); + for (const [page, pageMap] of this.groupedData_.entries()) { + if (page === AGGREGATE_KEY) continue; + const pageToMetricMap = getValueFromMap(page, submetrics); + for (const benchmark of this.orderedBenchmarks_) { + const benchmarkMap = pageMap.get(benchmark); + if (!benchmarkMap) continue; + for (const metric of METRICS.get(chartName)) { + const metricMap = benchmarkMap.get(metric); + const metricToSubmetricMap = getValueFromMap(metric, + pageToMetricMap); + const submetrics = metricMap.get(SUBMETRICS_KEY); + if (!submetrics) { + this.submetricsHelper_(metric, metricMap.get(STATISTICS_KEY), + benchmark, metricToSubmetricMap); + continue; + } + for (const [submetric, value] of [...submetrics]) { + this.submetricsHelper_(submetric, value, benchmark, + metricToSubmetricMap); + } + } + } + } + return submetrics; + }, + + sortByTotal_(a, b, chartName) { + if (a === AGGREGATE_KEY) return -1; + if (b === AGGREGATE_KEY) return 1; + let aValue = 0; + const aMap = this.groupedData_.get(a); + if (aMap.get(this.orderedBenchmarks_[0]) !== undefined) { + for (const metric of METRICS.get(chartName)) { + const aMetricMap = aMap.get(this.orderedBenchmarks_[0]).get(metric); + const aStats = aMetricMap.get(STATISTICS_KEY); + aValue += aStats ? aStats.mean : 0; + } + } + let bValue = 0; + const bMap = this.groupedData_.get(b); + if (bMap.get(this.orderedBenchmarks_[0]) !== undefined) { + for (const metric of METRICS.get(chartName)) { + const bMetricMap = bMap.get(this.orderedBenchmarks_[0]).get(metric); + const bStats = bMetricMap.get(STATISTICS_KEY); + bValue += bStats ? bStats.mean : 0; + } + } + return aValue - bValue; + }, + + filterPagesWithoutRasterMetric_(page) { + const pageMap = this.groupedData_.get(page); + for (const benchmark of this.orderedBenchmarks_) { + const pageMetricMap = pageMap.get(benchmark); + if (!pageMetricMap) continue; + const wantedMetric = pageMetricMap.get(RASTER_START_METRIC_KEY); + if (wantedMetric !== undefined) return true; + } + return false; + }, + + sortByRasterStart_(a, b) { + if (a === AGGREGATE_KEY) return 1; + if (b === AGGREGATE_KEY) return -1; + let aValue = 0; + const aMap = this.groupedData_.get(a); + if (aMap.get(this.orderedBenchmarks_[0]) !== undefined) { + const aMetricMap = aMap.get(this.orderedBenchmarks_[0]) + .get(RASTER_START_METRIC_KEY); + const aStats = aMetricMap.get(STATISTICS_KEY); + aValue = aStats ? aStats.mean : 0; + } + let bValue = 0; + const bMap = this.groupedData_.get(b); + if (bMap.get(this.orderedBenchmarks_[0]) !== undefined) { + const bMetricMap = bMap.get(this.orderedBenchmarks_[0]) + .get(RASTER_START_METRIC_KEY); + const bStats = bMetricMap.get(STATISTICS_KEY); + bValue = bStats ? bStats.mean : 0; + } + return bValue - aValue; + }, + }); + + return { + STATISTICS_KEY, + SUBMETRICS_KEY, + AGGREGATE_KEY, + COLORS, + FRAME, + METRICS, + getValueFromMap, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html new file mode 100644 index 00000000000..1199d8ed731 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2018 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> +<link rel="import" href="/tracing/value/ui/visualizations_data_container.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function getHistogram(name) { + const samples = []; + for (let i = 0; i < 5; ++i) { + const total = Math.random(); + const values = {}; + values[name + 'a'] = total / 2.0; + values[name + 'b'] = total / 4.0; + values[name + 'c'] = total / 4.0; + samples.push({ + value: total, + diagnostics: new Map([ + [ + tr.v.d.RESERVED_NAMES.BENCHMARK_START, + new tr.v.d.DateRange(Date.now()), + ], [ + 'breakdown', tr.v.d.Breakdown.fromDict({values}), + ], + ]), + }); + } + return tr.v.Histogram.create(name, tr.b.Unit.byName.count, samples); + } + + function getHistogramSet(displayLabel, story, containsRasterStart = true) { + const histograms = new tr.v.HistogramSet(); + let metrics = []; + for (const category of tr.v.ui.METRICS.values()) { + metrics = metrics.concat(category); + } + for (const metric of metrics) { + histograms.addHistogram(getHistogram(metric)); + } + + if (containsRasterStart) { + histograms.addHistogram( + getHistogram('pipeline:begin_frame_to_raster_start')); + histograms.addHistogram( + getHistogram('pipeline:begin_frame_to_raster_end')); + } + histograms.addSharedDiagnosticToAllHistograms( + tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet([displayLabel])); + histograms.addSharedDiagnosticToAllHistograms( + tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet([story])); + return histograms; + } + + test('instantiate', function() { + const cp = document.createElement('tr-v-ui-visualizations-data-container'); + this.addHTMLOutput(cp); + + const histograms = getHistogramSet('Run 1', 'test.com'); + + const histograms2 = getHistogramSet('Run 2', 'test.com'); + histograms.importDicts(histograms2.asDicts()); + + const histograms3 = getHistogramSet('Run 1', 'abc.com'); + histograms.importDicts(histograms3.asDicts()); + + const histograms4 = getHistogramSet('Run 2', 'abc.com'); + histograms.importDicts(histograms4.asDicts()); + + cp.build(histograms, histograms); + }); + + test('instantiateWithRepeat', function() { + const cp = document.createElement('tr-v-ui-visualizations-data-container'); + this.addHTMLOutput(cp); + + const histograms = getHistogramSet('Run 1', 'repeat.com'); + const histograms2 = getHistogramSet('Run 1', 'repeat.com'); + histograms.importDicts(histograms2.asDicts()); + + cp.build(histograms, histograms); + }); + + test('instantiateWithoutRasterTasks', function() { + const cp = document.createElement('tr-v-ui-visualizations-data-container'); + this.addHTMLOutput(cp); + + const histograms = getHistogramSet('Run 1', 'test.com', false); + + const histograms2 = getHistogramSet('Run 2', 'test.com', false); + histograms.importDicts(histograms2.asDicts()); + + const histograms3 = getHistogramSet('Run 1', 'abc.com'); + histograms.importDicts(histograms3.asDicts()); + + const histograms4 = getHistogramSet('Run 2', 'abc.com'); + histograms.importDicts(histograms4.asDicts()); + + cp.build(histograms, histograms); + }); + + test('instantiateWithDifferentStorySets', function() { + const cp = document.createElement('tr-v-ui-visualizations-data-container'); + this.addHTMLOutput(cp); + + const histograms = getHistogramSet('Run 1', 'test.com'); + + const histograms2 = getHistogramSet('Run 1', 'abc.com'); + histograms.importDicts(histograms2.asDicts()); + + const histograms3 = getHistogramSet('Run 2', 'abc.com'); + histograms.importDicts(histograms3.asDicts()); + + cp.build(histograms, histograms); + }); +}); +</script> |