summaryrefslogtreecommitdiffstats
path: root/chromium/third_party/catapult/tracing/tracing/value/ui
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/value/ui')
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span.html350
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/breakdown_span_test.html149
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/collected_related_event_set_span_test.html56
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span.html35
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/date_range_span_test.html30
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table.html125
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_map_table_test.html27
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span.html73
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/diagnostic_span_behavior.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span.html97
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/generic_set_span_test.html112
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram-set-view.md71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_importer_test.html101
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls.html557
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html300
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html251
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html290
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html459
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html396
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html361
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html299
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html1679
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html210
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html144
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html72
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html599
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html300
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html353
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html22
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html274
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html57
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html204
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html312
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html32
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html23
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html89
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html30
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html626
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html1027
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/timings.md78
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html41
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html410
-rw-r--r--chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html124
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_">&#945;=[[alphaString]]</button>
+ <div id="alpha_slider_container">
+ <input type="range" id="alpha_slider" value="{{alphaIndex::change}}" min="0" max="18" on-blur="closeAlphaSlider_" on-input="updateAlpha_">
+ </div>
+
+ <select id="statistic" value="{{displayStatisticName::change}}">
+ </select>
+
+ <button id="show_visualization" on-tap="loadVisualization_">Visualize</button>
+
+ <tr-ui-b-dropdown label="Export">
+ <tr-v-ui-histogram-set-controls-export>
+ </tr-v-ui-histogram-set-controls-export>
+ </tr-ui-b-dropdown>
+
+ <input type="checkbox" id="show_all" checked="{{showAll::change}}" title="When unchecked, less important histograms are hidden.">
+ <label for="show_all" title="When unchecked, less important histograms are hidden.">Show all</label>
+
+ <a id="help">Help</a>
+ <a id="feedback">Feedback</a>
+ </div>
+
+ <tr-ui-b-grouping-table-groupby-picker id="picker">
+ </tr-ui-b-grouping-table-groupby-picker>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ const ALPHA_OPTIONS = [];
+ for (let i = 1; i < 10; ++i) ALPHA_OPTIONS.push(i * 1e-3);
+ for (let i = 1; i < 10; ++i) ALPHA_OPTIONS.push(i * 1e-2);
+ ALPHA_OPTIONS.push(0.1);
+
+ Polymer({
+ is: 'tr-v-ui-histogram-set-controls',
+
+ properties: {
+ searchQuery: {
+ type: String,
+ value: '',
+ observer: 'onSearchQueryChange_',
+ },
+ showAll: {
+ type: Boolean,
+ value: true,
+ observer: 'onUserChange_',
+ },
+ referenceDisplayLabel: {
+ type: String,
+ value: '',
+ observer: 'onUserChange_',
+ },
+ displayStatisticName: {
+ type: String,
+ value: '',
+ observer: 'onUserChange_',
+ },
+ alphaString: {
+ type: String,
+ computed: 'getAlphaString_(alphaIndex)',
+ },
+ alphaIndex: {
+ type: Number,
+ value: 9,
+ observer: 'onUserChange_',
+ },
+ },
+
+ created() {
+ this.viewState_ = undefined;
+ this.rowListener_ = this.onRowViewStateUpdate_.bind(this);
+ this.baseStatisticNames_ = [];
+
+ // When onViewStateUpdate_() copies multiple properties from the viewState
+ // to polymer properties, disable onUserChange_ until all properties are
+ // copied in order to prevent nested mutations to the ViewState.
+ this.isInOnViewStateUpdate_ = false;
+ this.searchQueryDebounceMs = 200;
+ },
+
+ ready() {
+ this.$.picker.addEventListener('current-groups-changed',
+ this.onGroupsChanged_.bind(this));
+ },
+
+ get viewState() {
+ return this.viewState_;
+ },
+
+ set viewState(vs) {
+ if (this.viewState_) {
+ throw new Error('viewState must be set exactly once.');
+ }
+ this.viewState_ = vs;
+ this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this));
+ // It would be arduous to construct a delta and call viewStateListener_
+ // here in case vs contains non-default values, so callers must set
+ // viewState first and then update it.
+ },
+
+ async onSearchQueryChange_() {
+ // Bypass debouncing for testing purpose:
+ if (this.searchQueryDebounceMs === 0) return this.onUserChange_();
+ // Limit the update rate for instance caused by typing in a search.
+ this.debounce('onSearchQueryDebounce', this.onUserChange_,
+ this.searchQueryDebounceMs);
+ },
+
+ async onUserChange_() {
+ if (!this.viewState) return;
+ if (this.isInOnViewStateUpdate_) return;
+
+ const marks = [];
+ if (this.searchQuery !== this.viewState.searchQuery) {
+ marks.push(tr.b.Timing.mark('histogram-set-controls', 'search'));
+ }
+ if (this.showAll !== this.viewState.showAll) {
+ marks.push(tr.b.Timing.mark('histogram-set-controls', 'showAll'));
+ }
+ if (this.referenceDisplayLabel !== this.viewState.referenceDisplayLabel) {
+ marks.push(tr.b.Timing.mark(
+ 'histogram-set-controls', 'referenceColumn'));
+ }
+ if (this.displayStatisticName !== this.viewState.displayStatisticName) {
+ marks.push(tr.b.Timing.mark('histogram-set-controls', 'statistic'));
+ }
+ if (parseInt(this.alphaIndex) !== this.getAlphaIndexFromViewState_()) {
+ marks.push(tr.b.Timing.mark('histogram-set-controls', 'alpha'));
+ }
+
+ this.$.clear_search.style.visibility =
+ this.searchQuery ? 'visible' : 'hidden';
+
+ let displayStatisticName = this.displayStatisticName;
+ if (this.viewState.referenceDisplayLabel === '' &&
+ this.referenceDisplayLabel !== '' &&
+ this.baseStatisticNames.length) {
+ // The user selected a reference display label.
+ displayStatisticName = `%${tr.v.DELTA}${this.displayStatisticName}`;
+ // Can't set this.displayStatisticName before updating viewState -- that
+ // would cause an infinite loop of onUserChange_().
+ }
+ if (this.referenceDisplayLabel === '' &&
+ this.viewState.referenceDisplayLabel !== '' &&
+ this.baseStatisticNames.length) {
+ // The user unset the reference display label.
+ // Ensure that displayStatisticName is not a delta statistic.
+ const deltaIndex = displayStatisticName.indexOf(tr.v.DELTA);
+ if (deltaIndex >= 0) {
+ displayStatisticName = displayStatisticName.slice(deltaIndex + 1);
+ } else if (!this.baseStatisticNames.includes(displayStatisticName)) {
+ displayStatisticName = 'avg';
+ }
+ }
+
+ // Propagate updates from the user to the view state.
+ await this.viewState.update({
+ searchQuery: this.searchQuery,
+ showAll: this.showAll,
+ referenceDisplayLabel: this.referenceDisplayLabel,
+ displayStatisticName,
+ alpha: ALPHA_OPTIONS[this.alphaIndex],
+ });
+
+ if (this.referenceDisplayLabel &&
+ this.statisticNames.length === this.baseStatisticNames.length) {
+ // When a reference column is selected, delta statistics should be
+ // available.
+ this.statisticNames = this.baseStatisticNames.concat(
+ tr.v.Histogram.getDeltaStatisticsNames(this.baseStatisticNames));
+ } else if (!this.referenceDisplayLabel &&
+ this.statisticNames.length > this.baseStatisticNames.length) {
+ // When a reference column is not selected, delta statistics should not
+ // be available.
+ this.statisticNames = this.baseStatisticNames;
+ }
+
+ for (const mark of marks) mark.end();
+ },
+
+ onViewStateUpdate_(event) {
+ this.isInOnViewStateUpdate_ = true;
+
+ if (event.delta.searchQuery) {
+ this.searchQuery = this.viewState.searchQuery;
+ }
+
+ if (event.delta.showAll) this.showAll = this.viewState.showAll;
+
+ if (event.delta.displayStatisticName) {
+ this.displayStatisticName = this.viewState.displayStatisticName;
+ }
+
+ if (event.delta.referenceDisplayLabel) {
+ this.referenceDisplayLabel = this.viewState.referenceDisplayLabel;
+ this.$.alpha.style.display = this.referenceDisplayLabel ? 'inline' : '';
+ }
+
+ if (event.delta.groupings) {
+ this.$.picker.currentGroupKeys = this.viewState.groupings.map(
+ g => g.key);
+ }
+
+ if (event.delta.tableRowStates) {
+ for (const row of tr.v.ui.HistogramSetTableRowState.walkAll(
+ this.viewState.tableRowStates.values())) {
+ row.addUpdateListener(this.rowListener_);
+ }
+
+ const anyShowing = this.anyOverviewCharts_;
+ this.$.hide_overview.style.display = anyShowing ? 'inline' : 'none';
+ this.$.show_overview.style.display = anyShowing ? 'none' : 'inline';
+ }
+
+ if (event.delta.alpha) {
+ this.alphaIndex = this.getAlphaIndexFromViewState_();
+ }
+
+ this.isInOnViewStateUpdate_ = false;
+ this.onUserChange_();
+ },
+
+ onRowViewStateUpdate_(event) {
+ if (event.delta.isOverviewed) {
+ const anyShowing = event.delta.isOverviewed.current ||
+ this.anyOverviewCharts_;
+ this.$.hide_overview.style.display = anyShowing ? 'inline' : 'none';
+ this.$.show_overview.style.display = anyShowing ? 'none' : 'inline';
+ }
+
+ if (event.delta.subRows) {
+ for (const subRow of event.delta.subRows.previous) {
+ subRow.removeUpdateListener(this.rowListener_);
+ }
+ for (const subRow of event.delta.subRows.current) {
+ subRow.addUpdateListener(this.rowListener_);
+ }
+ }
+ },
+
+ onGroupsChanged_() {
+ if (this.$.picker.currentGroups.length === 0 &&
+ this.$.picker.possibleGroups.length > 0) {
+ // If the current groupings are now empty but there are possible
+ // groupings, then force there to be at least one grouping.
+ // The histogram-set-table requires there to be at least one grouping.
+ this.$.picker.currentGroupKeys = [this.$.picker.possibleGroups[0].key];
+ }
+ this.viewState.groupings = this.$.picker.currentGroups;
+ },
+
+ set showAllEnabled(enable) {
+ if (!enable) this.$.show_all.checked = true;
+ this.$.show_all.disabled = !enable;
+ },
+
+ set possibleGroupings(groupings) {
+ this.$.picker.possibleGroups = groupings;
+ this.$.picker.style.display = (groupings.length < 2) ? 'none' : 'block';
+ this.onGroupsChanged_();
+ },
+
+ set displayLabels(labels) {
+ this.$.reference_display_label.style.display =
+ (labels.length < 2) ? 'none' : 'inline';
+
+ while (this.$.reference_display_label.children.length > 1) {
+ this.$.reference_display_label.removeChild(
+ this.$.reference_display_label.lastChild);
+ }
+
+ for (const displayLabel of labels) {
+ const option = document.createElement('option');
+ option.textContent = displayLabel;
+ option.value = displayLabel;
+ this.$.reference_display_label.appendChild(option);
+ }
+
+ if (labels.includes(this.viewState.referenceDisplayLabel)) {
+ this.referenceDisplayLabel = this.viewState.referenceDisplayLabel;
+ } else {
+ this.viewState.referenceDisplayLabel = '';
+ }
+ },
+
+ get baseStatisticNames() {
+ return this.baseStatisticNames_;
+ },
+
+ set baseStatisticNames(names) {
+ this.baseStatisticNames_ = names;
+ this.statisticNames = names;
+ },
+
+ get statisticNames() {
+ return Array.from(this.$.statistic.options).map(o => o.value);
+ },
+
+ set statisticNames(names) {
+ this.$.statistic.style.display = (names.length < 2) ? 'none' : 'inline';
+
+ while (this.$.statistic.children.length) {
+ this.$.statistic.removeChild(this.$.statistic.lastChild);
+ }
+
+ for (const name of names) {
+ const option = document.createElement('option');
+ option.textContent = name;
+ this.$.statistic.appendChild(option);
+ }
+
+ if (names.includes(this.viewState.displayStatisticName)) {
+ this.displayStatisticName = this.viewState.displayStatisticName;
+ // Polymer doesn't reset the value when the options change, so do that
+ // manually.
+ this.$.statistic.value = this.displayStatisticName;
+ } else {
+ this.viewState.displayStatisticName = names[0] || '';
+ }
+ },
+
+ get anyOverviewCharts_() {
+ for (const row of tr.v.ui.HistogramSetTableRowState.walkAll(
+ this.viewState.tableRowStates.values())) {
+ if (row.isOverviewed) return true;
+ }
+ return false;
+ },
+
+ async toggleOverviewLineCharts_() {
+ const showOverviews = !this.anyOverviewCharts_;
+ const mark = tr.b.Timing.mark('histogram-set-controls',
+ (showOverviews ? 'show' : 'hide') + 'OverviewCharts');
+
+ for (const row of tr.v.ui.HistogramSetTableRowState.walkAll(
+ this.viewState.tableRowStates.values())) {
+ await row.update({isOverviewed: showOverviews});
+ }
+
+ this.$.hide_overview.style.display = showOverviews ? 'inline' : 'none';
+ this.$.show_overview.style.display = showOverviews ? 'none' : 'inline';
+
+ await tr.b.animationFrame();
+ mark.end();
+ },
+
+ set helpHref(href) {
+ this.$.help.href = href;
+ this.$.help.style.display = 'inline';
+ },
+
+ set feedbackHref(href) {
+ this.$.feedback.href = href;
+ this.$.feedback.style.display = 'inline';
+ },
+
+ clearSearch_() {
+ this.set('searchQuery', '');
+ this.$.search.focus();
+ },
+
+ getAlphaString_(alphaIndex) {
+ // (9 * 1e-3).toString() is "0.009000000000000001", so truncate.
+ return ('' + ALPHA_OPTIONS[alphaIndex]).substr(0, 5);
+ },
+
+ openAlphaSlider_() {
+ const alphaButtonRect = this.$.alpha.getBoundingClientRect();
+ this.$.alpha_slider_container.style.display = 'flex';
+ this.$.alpha_slider_container.style.top = alphaButtonRect.bottom + 'px';
+ this.$.alpha_slider_container.style.left = alphaButtonRect.left + 'px';
+ this.$.alpha_slider.focus();
+ },
+
+ closeAlphaSlider_() {
+ this.$.alpha_slider_container.style.display = '';
+ },
+
+ updateAlpha_() {
+ this.alphaIndex = this.$.alpha_slider.value;
+ },
+
+ getAlphaIndexFromViewState_() {
+ for (let i = 0; i < ALPHA_OPTIONS.length; ++i) {
+ if (ALPHA_OPTIONS[i] >= this.viewState.alpha) return i;
+ }
+ return ALPHA_OPTIONS.length - 1;
+ },
+
+ set enableVisualization(enable) {
+ this.$.show_visualization.style.display = enable ? 'inline' : 'none';
+ },
+
+ loadVisualization_() {
+ tr.b.dispatchSimpleEvent(this, 'loadVisualization', true, true, {});
+ },
+ });
+
+ return {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html
new file mode 100644
index 00000000000..98936f24bba
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_export.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+
+<dom-module id="tr-v-ui-histogram-set-controls-export">
+ <template>
+ <style>
+ :host {
+ display: grid;
+ grid-gap: 1em;
+ grid-template-rows: auto auto;
+ grid-template-columns: auto auto;
+ }
+ button {
+ -webkit-appearance: none;
+ border: 0;
+ font-size: initial;
+ padding: 5px;
+ }
+ </style>
+
+ <button on-tap="exportRawCsv_">raw CSV</button>
+ <button on-tap="exportRawJson_">raw JSON</button>
+ <button on-tap="exportMergedCsv_">merged CSV</button>
+ <button on-tap="exportMergedJson_">merged JSON</button>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ Polymer({
+ is: 'tr-v-ui-histogram-set-controls-export',
+
+ exportRawCsv_() {
+ this.export_(false, 'csv');
+ },
+
+ exportRawJson_() {
+ this.export_(false, 'json');
+ },
+
+ exportMergedCsv_() {
+ this.export_(true, 'csv');
+ },
+
+ exportMergedJson_() {
+ this.export_(true, 'json');
+ },
+
+ export_(merged, format) {
+ tr.b.dispatchSimpleEvent(this, 'export', true, true, {merged, format});
+ },
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html
new file mode 100644
index 00000000000..9783059ccbe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_controls_test.html
@@ -0,0 +1,300 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/value/histogram_grouping.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_controls.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ function buildControls(test) {
+ const controls = document.createElement('tr-v-ui-histogram-set-controls');
+ controls.viewState = new tr.v.ui.HistogramSetViewState();
+ test.addHTMLOutput(controls);
+ return controls;
+ }
+
+ test('helpHref', function() {
+ const controls = buildControls(this);
+ controls.helpHref = 'data:text/html,hello';
+ const help = tr.ui.b.findDeepElementMatchingPredicate(
+ controls, e => e.id === 'help');
+ assert.strictEqual(help.style.display, 'inline');
+ assert.strictEqual(help.href, 'data:text/html,hello');
+ });
+
+ test('feedbackHref', function() {
+ const controls = buildControls(this);
+ controls.feedbackHref = 'data:text/html,hello';
+ const feedback = tr.ui.b.findDeepElementMatchingPredicate(
+ controls, e => e.id === 'feedback');
+ assert.strictEqual(feedback.style.display, 'inline');
+ assert.strictEqual(feedback.href, 'data:text/html,hello');
+ });
+
+ test('displayLabels', function() {
+ const controls = buildControls(this);
+ const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'reference_display_label');
+ assert.strictEqual('none', getComputedStyle(selector).display);
+
+ controls.displayLabels = [];
+ assert.strictEqual('none', getComputedStyle(selector).display);
+
+ controls.displayLabels = ['Value'];
+ assert.strictEqual('none', getComputedStyle(selector).display);
+
+ controls.displayLabels = ['a', 'b\nc'];
+ assert.strictEqual('inline-block', getComputedStyle(selector).display);
+ assert.strictEqual('', selector.children[0].value);
+ assert.strictEqual('a', selector.children[1].value);
+ assert.strictEqual('a', selector.children[1].textContent);
+
+ // displayLabels can contain newlines, which <option> replace with spaces.
+ // histogram-set-controls must set option.value in order for selector.value
+ // to contain the newlines.
+ assert.strictEqual('b\nc', selector.children[2].value);
+ assert.strictEqual('b\nc', selector.children[2].textContent);
+ selector.selectedIndex = 2;
+ assert.strictEqual('b\nc', selector.value);
+
+ controls.displayLabels = ['Value'];
+ assert.strictEqual('none', getComputedStyle(selector).display);
+ });
+
+ test('baseStatisticNames', function() {
+ const controls = buildControls(this);
+ controls.baseStatisticNames = ['avg', 'std'];
+ const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'statistic');
+ assert.strictEqual('inline-block', getComputedStyle(selector).display);
+ assert.lengthOf(selector.children, 2);
+ assert.strictEqual('avg', selector.children[0].value);
+ assert.strictEqual('avg', selector.children[0].textContent);
+ assert.strictEqual('std', selector.children[1].value);
+ assert.strictEqual('std', selector.children[1].textContent);
+ assert.strictEqual('avg', selector.value);
+ });
+
+ test('viewDisplayStatisticName', function() {
+ const controls = buildControls(this);
+ controls.baseStatisticNames = ['avg', 'std'];
+ const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'statistic');
+ controls.viewState.displayStatisticName = 'std';
+ assert.strictEqual('std', selector.value);
+ });
+
+ test('controlDisplayStatisticName', function() {
+ const controls = buildControls(this);
+ controls.baseStatisticNames = ['avg', 'std'];
+ const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'statistic');
+ selector.value = 'std';
+ const changeEvent = document.createEvent('HTMLEvents');
+ changeEvent.initEvent('change', false, true);
+ selector.dispatchEvent(changeEvent);
+ assert.strictEqual('std', controls.viewState.displayStatisticName);
+ });
+
+ test('viewSearchQuery', function() {
+ const controls = buildControls(this);
+ controls.viewState.searchQuery = 'foo';
+ const search = tr.ui.b.findDeepElementMatchingPredicate(
+ controls, e => e.id === 'search');
+ assert.strictEqual(search.value, 'foo');
+ });
+
+ test('controlSearchQuery', function() {
+ const controls = buildControls(this);
+ controls.searchQueryDebounceMs = 0;
+ const search = tr.ui.b.findDeepElementMatching(controls, '#search');
+ search.value = 'x';
+ const keyupEvent = document.createEvent('KeyboardEvent');
+ keyupEvent.initEvent('keyup');
+ search.dispatchEvent(keyupEvent);
+ assert.strictEqual(controls.viewState.searchQuery, 'x');
+ controls.clearSearch_();
+ assert.strictEqual(controls.viewState.searchQuery, '');
+ });
+
+ test('viewShowAll', function() {
+ const controls = buildControls(this);
+ const showAll = tr.ui.b.findDeepElementMatchingPredicate(
+ controls, e => e.id === 'show_all');
+ assert.strictEqual(controls.viewState.showAll, true);
+ assert.strictEqual(showAll.checked, true);
+ controls.viewState.showAll = false;
+ assert.strictEqual(showAll.checked, false);
+ });
+
+ test('controlShowAll', function() {
+ const controls = buildControls(this);
+ const showAll = tr.ui.b.findDeepElementMatchingPredicate(
+ controls, e => e.id === 'show_all');
+ assert.strictEqual(controls.viewState.showAll, true);
+ assert.strictEqual(showAll.checked, true);
+ showAll.click();
+ assert.strictEqual(showAll.checked, false);
+ assert.strictEqual(controls.viewState.showAll, false);
+ const showAllLabel = tr.ui.b.findDeepElementMatchingPredicate(
+ controls, e => e.tagName === 'LABEL' && e.htmlFor === 'show_all');
+ showAllLabel.click();
+ assert.strictEqual(showAll.checked, true);
+ assert.strictEqual(controls.viewState.showAll, true);
+ });
+
+ test('viewReferenceDisplayLabel', function() {
+ const controls = buildControls(this);
+ controls.displayLabels = ['a', 'b'];
+ const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'reference_display_label');
+
+ assert.strictEqual('', selector.value);
+ assert.strictEqual('', controls.viewState.referenceDisplayLabel);
+
+ controls.viewState.referenceDisplayLabel = 'a';
+ assert.strictEqual('a', selector.value);
+
+ controls.viewState.referenceDisplayLabel = 'b';
+ assert.strictEqual('b', selector.value);
+
+ controls.viewState.referenceDisplayLabel = '';
+ assert.strictEqual('', selector.value);
+ });
+
+ test('controlReferenceDisplayLabel', function() {
+ const controls = buildControls(this);
+ controls.displayLabels = ['a', 'b'];
+ const selector = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'reference_display_label');
+ assert.strictEqual('', selector.value);
+ assert.strictEqual('', controls.viewState.referenceDisplayLabel);
+
+ selector.value = 'a';
+ const changeEvent = document.createEvent('HTMLEvents');
+ changeEvent.initEvent('change', false, true);
+ selector.dispatchEvent(changeEvent);
+ assert.strictEqual('a', controls.viewState.referenceDisplayLabel);
+
+ selector.value = 'b';
+ selector.dispatchEvent(changeEvent);
+ assert.strictEqual('b', controls.viewState.referenceDisplayLabel);
+
+ selector.value = '';
+ selector.dispatchEvent(changeEvent);
+ assert.strictEqual('', controls.viewState.referenceDisplayLabel);
+ });
+
+ test('viewGroupings', function() {
+ const controls = buildControls(this);
+ const fooGrouping = new tr.v.HistogramGrouping('foo', h => 'foo');
+ const groupings = Array.from(tr.v.HistogramGrouping.BY_KEY.values());
+ groupings.push(fooGrouping);
+ controls.possibleGroupings = groupings;
+ const picker = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.tagName === 'TR-UI-B-GROUPING-TABLE-GROUPBY-PICKER');
+ assert.lengthOf(picker.currentGroupKeys, 1);
+ assert.strictEqual(picker.currentGroupKeys[0],
+ tr.v.HistogramGrouping.HISTOGRAM_NAME.key);
+
+ controls.viewState.groupings = [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ ];
+ assert.lengthOf(picker.currentGroupKeys, 1);
+ assert.strictEqual(picker.currentGroupKeys[0],
+ tr.v.HistogramGrouping.HISTOGRAM_NAME.key);
+ assert.strictEqual('block', picker.style.display);
+
+ controls.viewState.groupings = [
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ fooGrouping,
+ ];
+ assert.lengthOf(picker.currentGroupKeys, 2);
+ assert.strictEqual(picker.currentGroupKeys[0],
+ tr.v.d.RESERVED_NAMES.STORIES);
+ assert.strictEqual(picker.currentGroupKeys[1], 'foo');
+ });
+
+ test('controlGroupings', function() {
+ const controls = buildControls(this);
+ const fooGrouping = new tr.v.HistogramGrouping('foo', h => 'foo');
+ const groupings = Array.from(tr.v.HistogramGrouping.BY_KEY.values());
+ groupings.push(fooGrouping);
+ controls.possibleGroupings = groupings;
+ const picker = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.tagName === 'TR-UI-B-GROUPING-TABLE-GROUPBY-PICKER');
+ assert.lengthOf(picker.currentGroupKeys, 1);
+ assert.strictEqual(controls.viewState.groupings[0].key,
+ tr.v.HistogramGrouping.HISTOGRAM_NAME.key);
+
+ picker.currentGroupKeys = ['name'];
+ assert.lengthOf(controls.viewState.groupings, 1);
+ assert.strictEqual(controls.viewState.groupings[0].key,
+ tr.v.HistogramGrouping.HISTOGRAM_NAME.key);
+
+ picker.currentGroupKeys = [tr.v.d.RESERVED_NAMES.STORIES, 'foo'];
+ assert.lengthOf(controls.viewState.groupings, 2);
+ assert.strictEqual(controls.viewState.groupings[0],
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES));
+ assert.strictEqual(controls.viewState.groupings[1],
+ fooGrouping);
+ });
+
+ test('viewIsOverviewed', function() {
+ const controls = buildControls(this);
+ const showOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'show_overview');
+ const hideOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'hide_overview');
+ controls.viewState.tableRowStates = new Map([
+ ['a', new tr.v.ui.HistogramSetTableRowState()],
+ ['b', new tr.v.ui.HistogramSetTableRowState()],
+ ]);
+ assert.strictEqual('inline', showOverview.style.display);
+ assert.strictEqual('none', hideOverview.style.display);
+
+ controls.viewState.tableRowStates.get('a').isOverviewed = true;
+ assert.strictEqual('none', showOverview.style.display);
+ assert.strictEqual('inline', hideOverview.style.display);
+
+ controls.viewState.tableRowStates.get('a').isOverviewed = false;
+ assert.strictEqual('inline', showOverview.style.display);
+ assert.strictEqual('none', hideOverview.style.display);
+ });
+
+ test('controlIsOverviewed', async function() {
+ const controls = buildControls(this);
+ const showOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'show_overview');
+ const hideOverview = tr.ui.b.findDeepElementMatchingPredicate(controls, e =>
+ e.id === 'hide_overview');
+ controls.viewState.tableRowStates = new Map([
+ ['a', new tr.v.ui.HistogramSetTableRowState()],
+ ['b', new tr.v.ui.HistogramSetTableRowState()],
+ ]);
+ assert.isFalse(controls.viewState.tableRowStates.get('a').isOverviewed);
+ assert.isFalse(controls.viewState.tableRowStates.get('b').isOverviewed);
+ assert.strictEqual('inline', showOverview.style.display);
+ assert.strictEqual('none', hideOverview.style.display);
+
+ await controls.toggleOverviewLineCharts_();
+ assert.strictEqual('none', showOverview.style.display);
+ assert.strictEqual('inline', hideOverview.style.display);
+ assert.isTrue(controls.viewState.tableRowStates.get('a').isOverviewed);
+ assert.isTrue(controls.viewState.tableRowStates.get('b').isOverviewed);
+
+ await controls.toggleOverviewLineCharts_();
+ assert.strictEqual('inline', showOverview.style.display);
+ assert.strictEqual('none', hideOverview.style.display);
+ assert.isFalse(controls.viewState.tableRowStates.get('a').isOverviewed);
+ assert.isFalse(controls.viewState.tableRowStates.get('b').isOverviewed);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html
new file mode 100644
index 00000000000..882806ba32b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location.html
@@ -0,0 +1,251 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/base/url_json.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ // This number is used to decide whether the tableStates can fit in the URL,
+ // and omit them if not.
+ // There is no specification for maximum URL length, so it is typically
+ // limited by hosts, not browsers.
+ // TODO(#3816) Tune this number.
+ const MAX_URL_LENGTH = 2048;
+
+ // This class wraps |window.location| and |window.history| to allow tests to
+ // mock it.
+ class Locus {
+ get origin() {
+ return window.location.origin;
+ }
+
+ get pathname() {
+ return window.location.pathname;
+ }
+
+ get search() {
+ return window.location.search;
+ }
+
+ get hash() {
+ return window.location.hash;
+ }
+
+ get state() {
+ if (this.stateMode === '#') return this.hash.substr(1);
+ return this.search.substr(1);
+ }
+
+ get stateMode() {
+ if (this.hash) return '#';
+ return '?';
+ }
+
+ buildUrlFromState(state) {
+ let url = this.origin + this.pathname;
+ if (this.stateMode === '#') url += this.search;
+ url += this.stateMode + state;
+ return url;
+ }
+
+ pushState(state) {
+ if (state === this.state) return;
+
+ // TODO(#3837) When should this actually call pushState()?
+ window.history.replaceState(null, null, this.buildUrlFromState(state));
+ }
+
+ addPopStateListener(listener) {
+ window.addEventListener('popstate', listener);
+ }
+ }
+
+ class HistogramSetLocation {
+ constructor(opt_location) {
+ // Optional dependency injection for testing.
+ this.location_ = opt_location || new Locus();
+ this.location_.addPopStateListener(this.onPopState_.bind(this));
+
+ this.viewState_ = undefined;
+ this.rowListener_ = this.onRowStateUpdate_.bind(this);
+ this.cellListener_ = this.onCellStateUpdate_.bind(this);
+
+ // pushState_ is disabled while handling onPopState_.
+ this.poppingState_ = false;
+ }
+
+ /**
+ * @return {!tr.v.ui.HistogramSetViewState}
+ */
+ get viewState() {
+ return this.viewState_;
+ }
+
+ /**
+ * @param {!tr.v.ui.HistogramSetViewState} vs
+ */
+ async build(vs) {
+ if (this.viewState !== undefined) {
+ throw new Error('viewState must be set exactly once.');
+ }
+ this.viewState_ = vs;
+ this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this));
+
+ await this.onPopState_();
+ }
+
+ onViewStateUpdate_(event) {
+ if (event.delta.tableRowStates) {
+ for (const row of tr.v.ui.HistogramSetTableRowState.walkAll(
+ event.delta.tableRowStates.previous.values())) {
+ row.removeUpdateListener(this.rowListener_);
+ for (const cell of row.cells.values()) {
+ cell.removeUpdateListener(this.cellListener_);
+ }
+ }
+ for (const row of tr.v.ui.HistogramSetTableRowState.walkAll(
+ event.delta.tableRowStates.current.values())) {
+ row.addUpdateListener(this.rowListener_);
+ for (const cell of row.cells.values()) {
+ cell.addUpdateListener(this.cellListener_);
+ }
+ }
+ }
+
+ this.pushState_();
+ }
+
+ onRowStateUpdate_(event) {
+ // This assumes that subRows and cells are not updated.
+ this.pushState_();
+ }
+
+ onCellStateUpdate_(event) {
+ this.pushState_();
+ }
+
+ pushState_() {
+ if (this.poppingState_) return;
+ const mark = tr.b.Timing.mark('HistogramSetLocation', 'pushState');
+
+ const params = new Map();
+ if (this.viewState.searchQuery) {
+ params.set('q', this.viewState.searchQuery);
+ }
+ if (this.viewState.referenceDisplayLabel) {
+ params.set('r', this.viewState.referenceDisplayLabel);
+ }
+ params.set('s', this.viewState.displayStatisticName);
+ if (!this.viewState.showAll) params.set('m', '');
+ params.set('g', this.viewState.groupings.map(g => g.key).join('.'));
+ if (this.viewState.sortColumnIndex !== undefined) {
+ params.set('c', '' + this.viewState.sortColumnIndex);
+ }
+ if (this.viewState.sortDescending) params.set('d', '');
+ if (!this.viewState.constrainNameColumn) params.set('n', '0');
+ if (!tr.b.math.approximately(this.viewState.alpha, 0.01)) {
+ params.set('p', ('' + this.viewState.alpha).substr(0, 5));
+ }
+
+ let urlState = '';
+ for (const [key, value] of params) {
+ if (urlState) urlState += '&';
+ urlState += key + '=' + window.encodeURIComponent(value);
+ }
+
+ const rowDicts = {};
+ for (const [name, rowState] of this.viewState.tableRowStates) {
+ const dict = rowState.asCompactDict();
+ if (dict === undefined) continue;
+ rowDicts[name] = dict;
+ }
+
+ if (Object.keys(rowDicts).length > 0) {
+ const rowsParam = '&t=' + tr.b.UrlJson.stringify(rowDicts);
+
+ if (this.location_.buildUrlFromState(urlState + rowsParam).length <
+ MAX_URL_LENGTH) {
+ urlState += rowsParam;
+ }
+ }
+
+ this.location_.pushState(urlState);
+ mark.end();
+ }
+
+ async onPopState_() {
+ const mark = tr.b.Timing.mark('HistogramSetLocation', 'onPopState');
+ this.poppingState_ = true;
+
+ const params = new Map();
+ for (const kvp of this.location_.state.split('&')) {
+ const [key, value] = kvp.split('=');
+ try {
+ params.set(key, window.decodeURIComponent(value));
+ } catch (e) {
+ // If the user tampers with the params so that a value cannot be
+ // decoded, ignore it.
+ }
+ }
+
+ const delta = new Map();
+ if (params.has('q')) delta.set('searchQuery', params.get('q'));
+ if (params.has('r')) delta.set('referenceDisplayLabel', params.get('r'));
+ if (params.has('s')) delta.set('displayStatisticName', params.get('s'));
+ delta.set('showAll', !params.has('m'));
+ if (params.has('g')) {
+ delta.set('groupings', params.get('g').split('.').map(
+ k => tr.v.HistogramGrouping.BY_KEY.get(k)));
+ }
+ if (params.has('c')) {
+ delta.set('sortColumnIndex', parseInt(params.get('c')));
+ } else {
+ delta.set('sortColumnIndex', 0);
+ }
+ delta.set('sortDescending', params.has('d'));
+ delta.set('constrainNameColumn', params.get('n') !== '0');
+ if (params.has('p')) {
+ delta.set('alpha', parseFloat(params.get('p')));
+ }
+
+ await this.viewState.update(delta);
+
+ if (params.has('t')) {
+ let rowDicts;
+ try {
+ rowDicts = tr.b.UrlJson.parse(params.get('t'));
+ } catch (e) {
+ // If the user tampers with the params so that rowDicts cannot be
+ // parsed, ignore it.
+ }
+
+ if (rowDicts) {
+ for (const [name, rowDict] of Object.entries(rowDicts)) {
+ const rowState = this.viewState.tableRowStates.get(name);
+ if (rowState === undefined) continue;
+ await rowState.updateFromCompactDict(rowDict);
+ }
+ }
+ }
+
+ this.poppingState_ = false;
+ mark.end();
+ }
+ }
+
+ HistogramSetLocation.Locus = Locus;
+
+ return {
+ HistogramSetLocation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html
new file mode 100644
index 00000000000..d9987e7b097
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_location_test.html
@@ -0,0 +1,290 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/value/ui/histogram_set_location.html">
+
+<script>
+'use strict';
+/* eslint-disable max-len */
+tr.b.unittest.testSuite(function() {
+ class TestLocus extends tr.v.ui.HistogramSetLocation.Locus {
+ constructor() {
+ super();
+ this.search_ = '';
+ this.hash_ = '';
+ this.listener_ = undefined;
+ }
+
+ get origin() {
+ return 'http://example.com';
+ }
+
+ get pathname() {
+ return '/pathname';
+ }
+
+ get search() {
+ return this.search_;
+ }
+
+ set search(s) {
+ this.search_ = s;
+ }
+
+ get hash() {
+ return this.hash_;
+ }
+
+ set hash(h) {
+ this.hash_ = h;
+ }
+
+ pushState(state) {
+ if (this.hash) {
+ this.hash = '#' + state;
+ } else {
+ this.search = '?' + state;
+ }
+ }
+
+ get stateMode() {
+ if (this.hash) return '#';
+ return '?';
+ }
+
+ addPopStateListener(listener) {
+ this.listener_ = listener;
+ }
+
+ async popState(state) {
+ if (state[0] === '?') {
+ this.search = state;
+ } else if (state[0] === '#') {
+ this.hash = state;
+ }
+ await this.listener_();
+ }
+ }
+
+ test('viewStateUpdateHashAndSearch', async function() {
+ const locus = new TestLocus();
+ locus.search = '?';
+ locus.hash = '#';
+ const hsl = new tr.v.ui.HistogramSetLocation(locus);
+ await hsl.build(new tr.v.ui.HistogramSetViewState());
+
+ await hsl.viewState.update({
+ displayStatisticName: 'avg',
+ groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ],
+ sortColumnIndex: undefined,
+ });
+ assert.strictEqual(locus.hash, '#s=avg&g=name.stories');
+ });
+
+ test('viewStateUpdateHash', async function() {
+ const locus = new TestLocus();
+ locus.hash = '#';
+ const hsl = new tr.v.ui.HistogramSetLocation(locus);
+ await hsl.build(new tr.v.ui.HistogramSetViewState());
+
+ await hsl.viewState.update({
+ displayStatisticName: 'avg',
+ groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ],
+ sortColumnIndex: undefined,
+ });
+ assert.strictEqual(locus.hash, '#s=avg&g=name.stories');
+
+ await hsl.viewState.update({searchQuery: 'foo'});
+ assert.strictEqual(locus.hash, '#q=foo&s=avg&g=name.stories');
+
+ await hsl.viewState.update({referenceDisplayLabel: 'bar'});
+ assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&g=name.stories');
+
+ await hsl.viewState.update({showAll: false});
+ assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&m=&g=name.stories');
+
+ await hsl.viewState.update({sortColumnIndex: 2});
+ assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2');
+
+ await hsl.viewState.update({sortDescending: true});
+ assert.strictEqual(locus.hash, '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=');
+
+ await hsl.viewState.update({constrainNameColumn: false});
+ assert.strictEqual(locus.hash,
+ '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0');
+
+ const rowState = new tr.v.ui.HistogramSetTableRowState();
+ rowState.cells.set('Value', new tr.v.ui.HistogramSetTableCellState());
+ await hsl.viewState.update({tableRowStates: new Map([['fmp', rowState]])});
+ assert.strictEqual(locus.hash,
+ '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0');
+
+ await hsl.viewState.tableRowStates.get('fmp').update({isExpanded: true});
+ assert.strictEqual(locus.hash,
+ '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1)');
+
+ await hsl.viewState.tableRowStates.get('fmp').cells.get('Value').update({
+ isOpen: true,
+ });
+ assert.strictEqual(locus.hash,
+ '#q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1.c-(Value-(o-1)))');
+ });
+
+ test('viewStateUpdateSearch', async function() {
+ const locus = new TestLocus();
+ const hsl = new tr.v.ui.HistogramSetLocation(locus);
+ await hsl.build(new tr.v.ui.HistogramSetViewState());
+
+ await hsl.viewState.update({
+ displayStatisticName: 'avg',
+ groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ],
+ sortColumnIndex: undefined,
+ });
+ assert.strictEqual(locus.search, '?s=avg&g=name.stories');
+
+ await hsl.viewState.update({searchQuery: 'foo'});
+ assert.strictEqual(locus.search, '?q=foo&s=avg&g=name.stories');
+
+ await hsl.viewState.update({referenceDisplayLabel: 'bar'});
+ assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&g=name.stories');
+
+ await hsl.viewState.update({showAll: false});
+ assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&m=&g=name.stories');
+
+ await hsl.viewState.update({sortColumnIndex: 2});
+ assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2');
+
+ await hsl.viewState.update({sortDescending: true});
+ assert.strictEqual(locus.search, '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=');
+
+ await hsl.viewState.update({constrainNameColumn: false});
+ assert.strictEqual(locus.search,
+ '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0');
+
+ const rowState = new tr.v.ui.HistogramSetTableRowState();
+ rowState.cells.set('Value', new tr.v.ui.HistogramSetTableCellState());
+ await hsl.viewState.update({tableRowStates: new Map([['fmp', rowState]])});
+ assert.strictEqual(locus.search,
+ '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0');
+
+ await hsl.viewState.tableRowStates.get('fmp').update({isExpanded: true});
+ assert.strictEqual(locus.search,
+ '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1)');
+
+ await hsl.viewState.tableRowStates.get('fmp').cells.get('Value').update({
+ isOpen: true,
+ });
+ assert.strictEqual(locus.search,
+ '?q=foo&r=bar&s=avg&m=&g=name.stories&c=2&d=&n=0&t=fmp-(e-1.c-(Value-(o-1)))');
+ });
+
+ test('popStateSearch', async function() {
+ const locus = new TestLocus();
+ const hsl = new tr.v.ui.HistogramSetLocation(locus);
+ await hsl.build(new tr.v.ui.HistogramSetViewState());
+
+ await locus.popState('?q=foo&r=bar&s=qux&m=&c=2&d=&g=name.stories');
+ assert.strictEqual('foo', hsl.viewState.searchQuery);
+ assert.strictEqual('bar', hsl.viewState.referenceDisplayLabel);
+ assert.strictEqual('qux', hsl.viewState.displayStatisticName);
+ assert.isFalse(hsl.viewState.showAll);
+ assert.lengthOf(hsl.viewState.groupings, 2);
+ assert.strictEqual('name', hsl.viewState.groupings[0].key);
+ assert.strictEqual('stories', hsl.viewState.groupings[1].key);
+ assert.strictEqual(2, hsl.viewState.sortColumnIndex);
+ assert.isTrue(hsl.viewState.sortDescending);
+
+ // onPopState_ should ignore missing rows and cells
+ await locus.popState('?t=f%3Am_p-(o-1)');
+ assert.strictEqual(0, hsl.viewState.tableRowStates.size);
+
+ await hsl.viewState.update({tableRowStates: new Map([
+ ['f:m_p', new tr.v.ui.HistogramSetTableRowState()],
+ ])});
+ assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').isExpanded);
+
+ await locus.popState('?t=f%3Am_p-(e-1)');
+ assert.strictEqual(0, hsl.viewState.tableRowStates.get('f:m_p').cells.size);
+ assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').isExpanded);
+
+ await hsl.viewState.tableRowStates.get('f:m_p').update({cells: new Map([
+ ['Value', new tr.v.ui.HistogramSetTableCellState()],
+ ])});
+ assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen);
+
+ await locus.popState('?t=f%3Am_p-(c-(Value-(o-1)))');
+ assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen);
+ });
+
+ test('popStateHashAndSearch', async function() {
+ const locus = new TestLocus();
+ const hsl = new tr.v.ui.HistogramSetLocation(locus);
+ await hsl.build(new tr.v.ui.HistogramSetViewState());
+
+ await locus.popState('?q=foo&r=bar&s=qux&a=&c=2&d=&g=name.stories');
+ assert.strictEqual('foo', hsl.viewState.searchQuery);
+ assert.strictEqual('bar', hsl.viewState.referenceDisplayLabel);
+ assert.strictEqual('qux', hsl.viewState.displayStatisticName);
+ assert.isTrue(hsl.viewState.showAll);
+ assert.lengthOf(hsl.viewState.groupings, 2);
+ assert.strictEqual('name', hsl.viewState.groupings[0].key);
+ assert.strictEqual('stories', hsl.viewState.groupings[1].key);
+ assert.strictEqual(2, hsl.viewState.sortColumnIndex);
+ assert.isTrue(hsl.viewState.sortDescending);
+
+ await locus.popState('#q=q');
+ assert.strictEqual('q', hsl.viewState.searchQuery);
+ });
+
+ test('popStateHash', async function() {
+ const locus = new TestLocus();
+ const hsl = new tr.v.ui.HistogramSetLocation(locus);
+ await hsl.build(new tr.v.ui.HistogramSetViewState());
+
+ await locus.popState('#q=foo&r=bar&s=qux&a=&c=2&d=&g=name.stories');
+ assert.strictEqual('foo', hsl.viewState.searchQuery);
+ assert.strictEqual('bar', hsl.viewState.referenceDisplayLabel);
+ assert.strictEqual('qux', hsl.viewState.displayStatisticName);
+ assert.isTrue(hsl.viewState.showAll);
+ assert.lengthOf(hsl.viewState.groupings, 2);
+ assert.strictEqual('name', hsl.viewState.groupings[0].key);
+ assert.strictEqual('stories', hsl.viewState.groupings[1].key);
+ assert.strictEqual(2, hsl.viewState.sortColumnIndex);
+ assert.isTrue(hsl.viewState.sortDescending);
+
+ // onPopState_ should ignore missing rows and cells
+ await locus.popState('#t=f%3Am_p-(o-1)');
+ assert.strictEqual(0, hsl.viewState.tableRowStates.size);
+
+ await hsl.viewState.update({tableRowStates: new Map([
+ ['f:m_p', new tr.v.ui.HistogramSetTableRowState()],
+ ])});
+ assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').isExpanded);
+
+ await locus.popState('#t=f%3Am_p-(e-1)');
+ assert.strictEqual(0, hsl.viewState.tableRowStates.get('f:m_p').cells.size);
+ assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').isExpanded);
+
+ await hsl.viewState.tableRowStates.get('f:m_p').update({cells: new Map([
+ ['Value', new tr.v.ui.HistogramSetTableCellState()],
+ ])});
+ assert.isFalse(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen);
+
+ await locus.popState('#t=f%3Am_p-(c-(Value-(o-1)))');
+ assert.isTrue(hsl.viewState.tableRowStates.get('f:m_p').cells.get('Value').isOpen);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html
new file mode 100644
index 00000000000..9ac3046c5d3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table.html
@@ -0,0 +1,459 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/histogram_set_hierarchy.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_table_row.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html">
+
+<dom-module id="tr-v-ui-histogram-set-table">
+ <template>
+ <style>
+ :host {
+ min-height: 0px;
+ overflow: auto;
+ }
+ #table {
+ margin-top: 5px;
+ }
+ </style>
+
+ <tr-ui-b-table id="table"/>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ const MIDLINE_HORIZONTAL_ELLIPSIS = String.fromCharCode(0x22ef);
+
+ // http://stackoverflow.com/questions/3446170
+ function escapeRegExp(str) {
+ return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
+ }
+
+ Polymer({
+ is: 'tr-v-ui-histogram-set-table',
+
+ created() {
+ this.viewState_ = undefined;
+ this.progress_ = () => Promise.resolve();
+ this.nameColumnTitle_ = undefined;
+ this.displayLabels_ = [];
+ this.histograms_ = undefined;
+ this.sourceHistograms_ = undefined;
+ this.filteredHistograms_ = undefined;
+ this.groupedHistograms_ = undefined;
+ this.hierarchies_ = undefined;
+ this.tableRows_ = undefined;
+
+ // Store this listener so it can be removed while updateContents_ modifies
+ // sortColumnIndex and sortDescending, then re-added.
+ this.sortColumnChangedListener_ = e => this.onSortColumnChanged_(e);
+ },
+
+ ready() {
+ this.$.table.zebra = true;
+ this.addEventListener('sort-column-changed',
+ this.sortColumnChangedListener_);
+ this.addEventListener('requestSelectionChange',
+ this.onRequestSelectionChange_.bind(this));
+ this.addEventListener('row-expanded-changed',
+ this.onRowExpandedChanged_.bind(this));
+ },
+
+ get viewState() {
+ return this.viewState_;
+ },
+
+ set viewState(vs) {
+ if (this.viewState_) {
+ throw new Error('viewState must be set exactly once.');
+ }
+ this.viewState_ = vs;
+ this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this));
+ // It would be arduous to construct a delta and call onViewStateUpdate_
+ // here in case vs contains non-default values, so callers must set
+ // viewState first and then update it.
+ },
+
+ get histograms() {
+ return this.histograms_;
+ },
+
+ /**
+ * @param {!tr.v.HistogramSet} histograms
+ * @param {!tr.v.HistogramSet} sourceHistograms
+ * @param {!Array.<string>} displayLabels
+ * @param {function(string, function())=} opt_progress
+ */
+ async build(histograms, sourceHistograms, displayLabels, opt_progress) {
+ this.histograms_ = histograms;
+ this.sourceHistograms_ = sourceHistograms;
+ this.filteredHistograms_ = undefined;
+ this.groupedHistograms_ = undefined;
+ this.displayLabels_ = displayLabels;
+
+ if (opt_progress !== undefined) this.progress_ = opt_progress;
+
+ if (histograms.length === 0) {
+ throw new Error('histogram-set-table requires non-empty HistogramSet.');
+ }
+
+ await this.progress_('Building columns...');
+ this.$.table.tableColumns = [
+ {
+ title: this.buildNameColumnTitle_(),
+ value: row => row.nameCell,
+ cmp: (a, b) => a.compareNames(b),
+ }
+ ].concat(displayLabels.map(l => this.buildColumn_(l)));
+
+ tr.b.Timing.instant('histogram-set-table', 'columnCount',
+ this.$.table.tableColumns.length);
+
+ // updateContents_() displays its own progress.
+ await this.updateContents_();
+
+ // Building some elements requires being able to measure them, which is
+ // impossible until they are displayed. If clients hide this table while
+ // it is being built, then they must display it when this event fires.
+ this.fire('display-ready');
+
+ this.progress_ = () => Promise.resolve();
+
+ this.checkNameColumnOverflow_(
+ tr.v.ui.HistogramSetTableRow.walkAll(this.$.table.tableRows));
+ },
+
+ buildNameColumnTitle_() {
+ this.nameColumnTitle_ = document.createElement('span');
+ this.nameColumnTitle_.style.display = 'inline-flex';
+
+ // Wrap the string in a span instead of using createTextNode() so that the
+ // span can be styled later.
+ const nameEl = document.createElement('span');
+ nameEl.textContent = 'Name';
+ this.nameColumnTitle_.appendChild(nameEl);
+
+ const toggleWidthEl = document.createElement('span');
+ toggleWidthEl.style.fontWeight = 'bold';
+ toggleWidthEl.style.background = '#bbb';
+ toggleWidthEl.style.color = '#333';
+ toggleWidthEl.style.padding = '0px 3px';
+ toggleWidthEl.style.marginRight = '8px';
+ toggleWidthEl.style.display = 'none';
+ toggleWidthEl.textContent = MIDLINE_HORIZONTAL_ELLIPSIS;
+ toggleWidthEl.addEventListener('click',
+ this.toggleNameColumnWidth_.bind(this));
+ this.nameColumnTitle_.appendChild(toggleWidthEl);
+ return this.nameColumnTitle_;
+ },
+
+ toggleNameColumnWidth_(opt_event) {
+ this.viewState.update({
+ constrainNameColumn: !this.viewState.constrainNameColumn,
+ });
+
+ if (opt_event !== undefined) {
+ opt_event.stopPropagation();
+ opt_event.preventDefault();
+ tr.b.Timing.instant('histogram-set-table', 'nameColumn' +
+ (this.viewState.constrainNameColumn ? 'Constrained' :
+ 'Unconstrained'));
+ }
+ },
+
+ buildColumn_(displayLabel) {
+ const title = document.createElement('span');
+ title.textContent = displayLabel;
+ title.style.whiteSpace = 'pre';
+
+ return {
+ displayLabel,
+ title,
+ value: row => row.getCell(displayLabel),
+ cmp: (rowA, rowB) => rowA.compareCells(rowB, displayLabel),
+ };
+ },
+
+ async updateContents_() {
+ const previousRowStates = this.viewState.tableRowStates;
+
+ if (!this.filteredHistograms_) {
+ await this.progress_('Filtering rows...');
+ this.filteredHistograms_ = this.viewState.showAll ?
+ this.histograms : this.sourceHistograms_;
+
+ if (this.viewState.searchQuery) {
+ let query;
+ try {
+ query = new RegExp(this.viewState.searchQuery);
+ } catch (e) {
+ }
+ if (query !== undefined) {
+ this.filteredHistograms_ = new tr.v.HistogramSet(
+ [...this.filteredHistograms_].filter(
+ hist => hist.name.match(query)));
+ if (this.filteredHistograms_.length === 0 &&
+ !this.viewState.showAll) {
+ await this.viewState.update({showAll: true});
+ return;
+ }
+ }
+ }
+ this.groupedHistograms_ = undefined;
+ }
+
+ if (!this.groupedHistograms_) {
+ await this.progress_('Grouping Histograms...');
+ this.groupHistograms_();
+ }
+
+ if (!this.hierarchies_) {
+ await this.progress_('Merging Histograms...');
+ this.hierarchies_ = tr.v.HistogramSetHierarchy.build(
+ this.groupedHistograms_);
+ this.tableRows_ = undefined;
+ }
+
+ const tableRowsDirty = this.tableRows_ === undefined;
+ if (tableRowsDirty) {
+ // Wait to set this.$.table.tableRows until we're ready for it to build
+ // DOM. When tableRows are set on it, tr-ui-b-table calls
+ // setTimeout(..., 0) to schedule rebuild for the next interpreter tick,
+ // but that can happen in between the next await, which is too early.
+ this.tableRows_ = this.hierarchies_.map(hierarchy =>
+ new tr.v.ui.HistogramSetTableRow(
+ hierarchy, this.$.table, this.viewState));
+
+ tr.b.Timing.instant('histogram-set-table', 'rootRowCount',
+ this.tableRows_.length);
+
+ const namesToRowStates = new Map();
+ for (const row of this.tableRows_) {
+ namesToRowStates.set(row.name, row.viewState);
+ }
+ await this.viewState.update({tableRowStates: namesToRowStates});
+ }
+
+ await this.progress_('Configuring table...');
+ this.nameColumnTitle_.children[1].style.filter =
+ this.viewState.constrainNameColumn ? 'invert(100%)' : '';
+
+ const referenceDisplayLabelIndex = this.displayLabels_.indexOf(
+ this.viewState.referenceDisplayLabel);
+ this.$.table.selectedTableColumnIndex = (referenceDisplayLabelIndex < 0) ?
+ undefined : (1 + referenceDisplayLabelIndex);
+
+ // Temporarily stop listening for this event in order to prevent the
+ // listener from updating viewState unnecessarily.
+ this.removeEventListener('sort-column-changed',
+ this.sortColumnChangedListener_);
+ this.$.table.sortColumnIndex = this.viewState.sortColumnIndex;
+ this.$.table.sortDescending = this.viewState.sortDescending;
+ this.addEventListener('sort-column-changed',
+ this.sortColumnChangedListener_);
+
+ // Each name-cell listens to this.viewState for updates to
+ // constrainNameColumn.
+ // Each table-cell listens to this.viewState for updates to
+ // displayStatisticName and referenceDisplayLabel.
+
+ if (tableRowsDirty) {
+ await this.progress_('Building DOM...');
+ this.$.table.tableRows = this.tableRows_;
+
+ // Try to restore previous row state.
+ // Wait to do this until after the base table has the new rows so that
+ // setExpandedForTableRow doesn't get confused.
+ for (const row of this.tableRows_) {
+ const previousState = previousRowStates.get(row.name);
+ if (!previousState) continue;
+ await row.restoreState(previousState);
+ }
+ }
+
+ // It's always safe to call this, it will only recompute what is dirty.
+ // We want to make sure that the table is up to date when this async
+ // function resolves.
+ this.$.table.rebuild();
+ },
+
+ async onRowExpandedChanged_(event) {
+ event.row.viewState.isExpanded =
+ this.$.table.getExpandedForTableRow(event.row);
+ tr.b.Timing.instant('histogram-set-table',
+ 'row' + (event.row.viewState.isExpanded ? 'Expanded' : 'Collapsed'));
+
+ // When the user expands a row, the table builds subRows' name-cells.
+ // If a subRow's name isOverflowing even though none of the top-level rows
+ // are constrained, show the dots to allow the user to unconstrain the
+ // name column.
+ // Each name-cell.isOverflowing would force layout if we don't await
+ // animationFrame here, which would be inefficient.
+ if (this.nameColumnTitle_.children[1].style.display === 'block') return;
+ await tr.b.animationFrame();
+ this.checkNameColumnOverflow_(event.row.subRows);
+ },
+
+ checkNameColumnOverflow_(rows) {
+ for (const row of rows) {
+ if (!row.nameCell.isOverflowing) continue;
+
+ const [nameSpan, dots] = this.nameColumnTitle_.children;
+ dots.style.display = 'block';
+
+ // Size the span containing 'Name' so that the dots align with the
+ // ellipses in the name-cells.
+ const labelWidthPx = tr.v.ui.NAME_COLUMN_WIDTH_PX -
+ dots.getBoundingClientRect().width;
+ nameSpan.style.width = labelWidthPx + 'px';
+
+ return;
+ }
+ },
+
+ groupHistograms_() {
+ const groupings = this.viewState.groupings.slice();
+ groupings.push(tr.v.HistogramGrouping.DISPLAY_LABEL);
+
+ function canSkipGrouping(grouping, groupedHistograms) {
+ // Never skip meaningful groupings.
+ if (groupedHistograms.size > 1) return false;
+
+ // Never skip the zero-th grouping.
+ if (grouping.key === groupings[0].key) return false;
+
+ // Never skip the grouping that defines the table columns.
+ if (grouping.key === tr.v.HistogramGrouping.DISPLAY_LABEL.key) {
+ return false;
+ }
+
+ // Skip meaningless groupings.
+ return true;
+ }
+
+ this.groupedHistograms_ =
+ this.filteredHistograms_.groupHistogramsRecursively(
+ groupings, canSkipGrouping);
+
+ this.hierarchies_ = undefined;
+ },
+
+ /**
+ * @param {!tr.b.Event} event
+ * @param {!Object} event.delta
+ * @param {!Object} event.delta.searchQuery
+ * @param {!Object} event.delta.referenceDisplayLabel
+ * @param {!Object} event.delta.displayStatisticName
+ * @param {!Object} event.delta.showAll
+ * @param {!Object} event.delta.groupings
+ * @param {!Object} event.delta.sortColumnIndex
+ * @param {!Object} event.delta.sortDescending
+ * @param {!Object} event.delta.constrainNameColumn
+ * @param {!Object} event.delta.tableRowStates
+ */
+ async onViewStateUpdate_(event) {
+ if (this.histograms_ === undefined) return;
+
+ if (event.delta.searchQuery !== undefined ||
+ event.delta.showAll !== undefined) {
+ this.filteredHistograms_ = undefined;
+ }
+
+ if (event.delta.groupings !== undefined) {
+ this.groupedHistograms_ = undefined;
+ }
+
+ if (event.delta.displayStatistic !== undefined &&
+ this.$.table.sortColumnIndex > 0) {
+ // Force re-sort.
+ this.$.table.sortColumnIndex = undefined;
+ }
+
+ if (event.delta.referenceDisplayLabel !== undefined ||
+ event.delta.displayStatisticName !== undefined) {
+ // Force this.$.table.bodyDirty_ = true;
+ this.$.table.tableRows = this.$.table.tableRows;
+ }
+
+ // updateContents_() always copies sortColumnIndex and sortDescending
+ // from the viewState to the table. The table will only re-sort if
+ // they change.
+
+ // Name-cells listen to this.viewState to handle updates to
+ // constrainNameColumn.
+
+ if (event.delta.tableRowStates) {
+ if (this.tableRows_.length !==
+ this.viewState.tableRowStates.size) {
+ throw new Error(
+ 'Only histogram-set-table may update tableRowStates');
+ }
+ for (const row of this.tableRows_) {
+ if (this.viewState.tableRowStates.get(row.name) !== row.viewState) {
+ throw new Error(
+ 'Only histogram-set-table may update tableRowStates');
+ }
+ }
+ return; // No need to re-enter updateContents_().
+ }
+
+ await this.updateContents_();
+ },
+
+ onSortColumnChanged_(event) {
+ tr.b.Timing.instant('histogram-set-table', 'sortColumn');
+ this.viewState.update({
+ sortColumnIndex: event.sortColumnIndex,
+ sortDescending: event.sortDescending,
+ });
+ },
+
+ onRequestSelectionChange_(event) {
+ // This event may reference an EventSet or an array of Histogram names.
+ // If EventSet, let the BrushingStateController handle it.
+ if (event.selection instanceof tr.model.EventSet) return;
+
+ event.stopPropagation();
+ tr.b.Timing.instant('histogram-set-table', 'selectHistogramNames');
+
+ let histogramNames = event.selection;
+ histogramNames.sort();
+ histogramNames = histogramNames.map(escapeRegExp).join('|');
+ this.viewState.update({
+ showAll: true,
+ searchQuery: `^(${histogramNames})$`,
+ });
+ },
+
+ /**
+ * @return {!tr.v.HistogramSet}
+ */
+ get leafHistograms() {
+ const histograms = new tr.v.HistogramSet();
+ for (const row of
+ tr.v.ui.HistogramSetTableRow.walkAll(this.$.table.tableRows)) {
+ if (row.subRows.length) continue;
+ for (const hist of row.columns.values()) {
+ if (!(hist instanceof tr.v.Histogram)) continue;
+
+ histograms.addHistogram(hist);
+ }
+ }
+ return histograms;
+ }
+ });
+
+ return {
+ MIDLINE_HORIZONTAL_ELLIPSIS,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html
new file mode 100644
index 00000000000..8a1d158d021
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_cell.html
@@ -0,0 +1,396 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/base/name_line_chart.html">
+<link rel="import" href="/tracing/value/ui/histogram_span.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id="tr-v-ui-histogram-set-table-cell">
+ <template>
+ <style>
+ #histogram_container {
+ display: flex;
+ flex-direction: row;
+ }
+
+ #missing, #empty, #unmergeable, #scalar {
+ flex-grow: 1;
+ }
+
+ #open_histogram, #close_histogram, #open_histogram svg, #close_histogram svg {
+ height: 1em;
+ }
+
+ #open_histogram svg {
+ margin-left: 4px;
+ stroke-width: 0;
+ stroke: blue;
+ fill: blue;
+ }
+ :host(:hover) #open_histogram svg {
+ background: blue;
+ stroke: white;
+ fill: white;
+ }
+
+ #scalar {
+ flex-grow: 1;
+ white-space: nowrap;
+ }
+
+ #histogram {
+ flex-grow: 1;
+ }
+
+ #close_histogram svg line {
+ stroke-width: 18;
+ stroke: black;
+ }
+ #close_histogram:hover svg {
+ background: black;
+ }
+ #close_histogram:hover svg line {
+ stroke: white;
+ }
+
+ #overview_container {
+ display: none;
+ }
+ </style>
+
+ <div id="histogram_container">
+ <span id="missing">(missing)</span>
+ <span id="empty">(empty)</span>
+ <span id="unmergeable">(unmergeable)</span>
+
+ <tr-v-ui-scalar-span id="scalar" on-click="openHistogram_"></tr-v-ui-scalar-span>
+
+ <span id="open_histogram" on-click="openHistogram_">
+ <svg viewbox="0 0 128 128">
+ <rect x="16" y="24" width="32" height="16"/>
+ <rect x="16" y="56" width="96" height="16"/>
+ <rect x="16" y="88" width="64" height="16"/>
+ </svg>
+ </span>
+
+ <span id="histogram"></span>
+
+ <span id="close_histogram" on-click="closeHistogram_">
+ <svg viewbox="0 0 128 128">
+ <line x1="28" y1="28" x2="100" y2="100"/>
+ <line x1="28" y1="100" x2="100" y2="28"/>
+ </svg>
+ </span>
+ </div>
+
+ <div id="overview_container">
+ </div>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ Polymer({
+ is: 'tr-v-ui-histogram-set-table-cell',
+
+ created() {
+ this.viewState_ = undefined;
+ this.rootListener_ = this.onRootStateUpdate_.bind(this);
+ this.row_ = undefined;
+ this.displayLabel_ = '';
+ this.histogram_ = undefined;
+ this.histogramSpan_ = undefined;
+ this.overviewChart_ = undefined;
+ this.mwuResult_ = undefined;
+ },
+
+ ready() {
+ this.addEventListener('click', this.onClick_.bind(this));
+ },
+
+ attached() {
+ if (this.row) {
+ this.row.rootViewState.addUpdateListener(this.rootListener_);
+ }
+ },
+
+ detached() {
+ this.row.rootViewState.removeUpdateListener(this.rootListener_);
+ // Don't need to removeUpdateListener for the row and cells; their
+ // lifetimes are the same as |this|.
+ },
+
+ updateMwu_() {
+ const referenceHistogram = this.referenceHistogram;
+ this.mwuResult_ = undefined;
+ if (!(this.histogram instanceof tr.v.Histogram)) return;
+ if (!this.histogram.canCompare(referenceHistogram)) return;
+ this.mwuResult_ = tr.b.math.Statistics.mwu(
+ this.histogram.sampleValues,
+ referenceHistogram.sampleValues,
+ this.row.rootViewState.alpha);
+ },
+
+ build(row, displayLabel, viewState) {
+ this.row_ = row;
+ this.displayLabel_ = displayLabel;
+ this.viewState_ = viewState;
+ this.histogram_ = this.row.columns.get(displayLabel);
+
+ if (this.viewState) {
+ // this.viewState is undefined when this.histogram_ is undefined.
+ // In that case, onViewStateUpdate_ wouldn't be able to do anything
+ // anyway.
+ this.viewState.addUpdateListener(this.onViewStateUpdate_.bind(this));
+ }
+ this.row.viewState.addUpdateListener(this.onRowStateUpdate_.bind(this));
+ if (this.isAttached) {
+ this.row.rootViewState.addUpdateListener(this.rootListener_);
+ }
+
+ this.updateMwu_();
+
+ // this.histogram_ and this.referenceHistogram might be undefined,
+ // a HistogramSet of unmergeable Histograms, or a Histogram.
+ this.updateContents_();
+ },
+
+ updateSignificance_() {
+ if (!this.mwuResult_) return;
+ this.$.scalar.significance = this.mwuResult_.significance;
+ },
+
+ get viewState() {
+ return this.viewState_;
+ },
+
+ get row() {
+ return this.row_;
+ },
+
+ get histogram() {
+ return this.histogram_;
+ },
+
+ get referenceHistogram() {
+ const referenceDisplayLabel =
+ this.row.rootViewState.referenceDisplayLabel;
+ if (!referenceDisplayLabel) return undefined;
+ if (referenceDisplayLabel === this.displayLabel_) return undefined;
+ return this.row.columns.get(referenceDisplayLabel);
+ },
+
+ get isHistogramOpen() {
+ return (this.histogramSpan_ !== undefined) &&
+ (this.$.histogram.style.display === 'block');
+ },
+
+ set isHistogramOpen(open) {
+ if (!(this.histogram instanceof tr.v.Histogram) ||
+ (this.histogram.numValues === 0)) {
+ return;
+ }
+
+ // Unfortunately, we can't use a css attribute for this since this stuff
+ // is tied up in all the possible states of this.histogram. See
+ // updateContents_().
+
+ this.$.scalar.style.display = open ? 'none' : 'flex';
+ this.$.open_histogram.style.display = open ? 'none' : 'block';
+
+ this.$.close_histogram.style.display = open ? 'block' : 'none';
+ this.$.histogram.style.display = open ? 'block' : 'none';
+
+ // Wait to create the histogram-span until the user wants to display it
+ // in order to speed up creating lots of histogram-set-table-cells when
+ // building the table.
+ if (open && this.histogramSpan_ === undefined) {
+ this.histogramSpan_ = document.createElement('tr-v-ui-histogram-span');
+ this.histogramSpan_.viewState = this.viewState;
+ this.histogramSpan_.rowState = this.row.viewState;
+ this.histogramSpan_.rootState = this.row.rootViewState;
+ this.histogramSpan_.build(this.histogram, this.referenceHistogram);
+ this.$.histogram.appendChild(this.histogramSpan_);
+ }
+
+ this.viewState.isOpen = open;
+ },
+
+ onViewStateUpdate_(event) {
+ if (event.delta.isOpen) {
+ this.isHistogramOpen = this.viewState.isOpen;
+ }
+ },
+
+ onRowStateUpdate_(event) {
+ if (event.delta.isOverviewed === undefined) return;
+ if (this.row.viewState.isOverviewed) {
+ this.showOverview();
+ } else {
+ this.hideOverview();
+ }
+ },
+
+ onRootStateUpdate_(event) {
+ if (event.delta.referenceDisplayLabel &&
+ this.histogramSpan_) {
+ this.histogramSpan_.build(this.histogram, this.referenceHistogram);
+ }
+
+ if (event.delta.displayStatisticName ||
+ event.delta.referenceDisplayLabel) {
+ this.updateMwu_();
+ this.updateContents_();
+ } else if (event.delta.alpha && this.mwuResult_) {
+ this.mwuResult_.compare(this.row.rootViewState.alpha);
+ this.updateSignificance_();
+ }
+
+ if (this.row.viewState.isOverviewed &&
+ (event.delta.sortColumnIndex ||
+ event.delta.sortDescending ||
+ event.delta.displayStatisticName ||
+ event.delta.referenceDisplayLabel)) {
+ if (this.overviewChart_ !== undefined) {
+ this.$.overview_container.removeChild(this.overviewChart_);
+ this.overviewChart_ = undefined;
+ }
+ this.showOverview();
+ }
+ },
+
+ onClick_(event) {
+ // Since the histogram-set-table's table doesn't support any kind of
+ // selection, clicking anywhere within a row that has subRows will
+ // expand/collapse that row, which can relayout the table and move things
+ // around. Prevent table relayout by preventing the tr-ui-b-table from
+ // receiving the click event.
+ event.stopPropagation();
+ },
+
+ openHistogram_() {
+ this.isHistogramOpen = true;
+ tr.b.Timing.instant('histogram-set-table-cell', 'open');
+ },
+
+ closeHistogram_() {
+ this.isHistogramOpen = false;
+ tr.b.Timing.instant('histogram-set-table-cell', 'close');
+ },
+
+ updateContents_() {
+ const isOpen = this.isHistogramOpen;
+
+ this.$.empty.style.display = 'none';
+ this.$.unmergeable.style.display = 'none';
+ this.$.scalar.style.display = 'none';
+ this.$.histogram.style.display = 'none';
+ this.$.close_histogram.style.display = 'none';
+ this.$.open_histogram.style.visibility = 'hidden';
+
+ if (!this.histogram) {
+ this.$.missing.style.display = 'block';
+ return;
+ }
+
+ this.$.missing.style.display = 'none';
+
+ if (this.histogram instanceof tr.v.HistogramSet) {
+ this.$.unmergeable.style.display = 'block';
+ return;
+ }
+
+ if (!(this.histogram instanceof tr.v.Histogram)) {
+ throw new Error('Invalid Histogram: ' + this.histogram);
+ }
+
+ if (this.histogram.numValues === 0) {
+ this.$.empty.style.display = 'block';
+ return;
+ }
+
+ this.$.open_histogram.style.display = 'block';
+ this.$.open_histogram.style.visibility = 'visible';
+ this.$.scalar.style.display = 'flex';
+
+ this.updateSignificance_();
+
+ const referenceHistogram = this.referenceHistogram;
+ const statName = this.histogram.getAvailableStatisticName(
+ this.row.rootViewState.displayStatisticName, referenceHistogram);
+ const statisticScalar = this.histogram.getStatisticScalar(
+ statName, referenceHistogram);
+ this.$.scalar.setValueAndUnit(
+ statisticScalar.value, statisticScalar.unit);
+
+ this.isHistogramOpen = isOpen;
+ },
+
+ showOverview() {
+ this.$.overview_container.style.display = 'block';
+ if (this.overviewChart_ !== undefined) return;
+
+ this.row.sortSubRows();
+ let referenceDisplayLabel =
+ this.row.rootViewState.referenceDisplayLabel;
+ if (referenceDisplayLabel === this.displayLabel_) {
+ referenceDisplayLabel = undefined;
+ }
+ const displayStatisticName = this.row.rootViewState.displayStatisticName;
+ const data = [];
+ let unit;
+
+ for (const subRow of this.row.subRows) {
+ const subHist = subRow.columns.get(this.displayLabel_);
+ if (!(subHist instanceof tr.v.Histogram)) continue;
+
+ if (unit === undefined) {
+ unit = subHist.unit;
+ } else if (unit !== subHist.unit) {
+ // The subrows have different units, so the overview chart cannot
+ // use a single unit to format all of the values, so don't display
+ // an overview chart at all.
+ data.splice(0);
+ break;
+ }
+
+ const refHist = subRow.columns.get(referenceDisplayLabel);
+ const statName = subHist.getAvailableStatisticName(
+ displayStatisticName, refHist);
+ const statScalar = subHist.getStatisticScalar(
+ statName, refHist);
+
+ if (statScalar !== undefined) {
+ data.push({
+ x: subRow.name,
+ y: statScalar.value,
+ });
+ }
+ }
+ if (data.length < 2) return;
+
+ this.overviewChart_ = new tr.ui.b.NameLineChart();
+ this.$.overview_container.appendChild(this.overviewChart_);
+ this.overviewChart_.displayXInHover = true;
+ this.overviewChart_.hideLegend = true;
+ this.overviewChart_.unit = unit;
+ this.overviewChart_.overrideDataRange = this.row.overviewDataRange;
+ this.overviewChart_.data = data;
+ },
+
+ hideOverview() {
+ this.$.overview_container.style.display = 'none';
+ }
+ });
+
+ return {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html
new file mode 100644
index 00000000000..f0dec062018
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_name_cell.html
@@ -0,0 +1,361 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/ui/base/name_line_chart.html">
+
+<dom-module id="tr-v-ui-histogram-set-table-name-cell">
+ <template>
+ <style>
+ #name_container {
+ display: flex;
+ }
+
+ #name {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ #show_overview, #hide_overview, #show_overview svg, #hide_overview svg {
+ height: 1em;
+ margin-left: 5px;
+ }
+
+ #show_overview svg {
+ stroke: blue;
+ stroke-width: 16;
+ }
+
+ #show_overview:hover svg {
+ background: blue;
+ stroke: white;
+ }
+
+ #hide_overview {
+ display: none;
+ }
+
+ #hide_overview svg {
+ stroke-width: 18;
+ stroke: black;
+ }
+
+ #hide_overview:hover svg {
+ background: black;
+ stroke: white;
+ }
+
+ #open_histograms, #close_histograms, #open_histograms svg, #close_histograms svg {
+ height: 1em;
+ }
+
+ #close_histograms {
+ display: none;
+ }
+
+ #open_histograms svg {
+ margin-left: 4px;
+ stroke-width: 0;
+ stroke: blue;
+ fill: blue;
+ }
+ #open_histograms:hover svg {
+ background: blue;
+ stroke: white;
+ fill: white;
+ }
+
+ #close_histograms line {
+ stroke-width: 18;
+ stroke: black;
+ }
+ #close_histograms:hover {
+ background: black;
+ }
+ #close_histograms:hover line {
+ stroke: white;
+ }
+
+ #overview_container {
+ display: none;
+ }
+ </style>
+
+ <div id="name_container">
+ <span id="name"></span>
+
+ <span id="show_overview" on-click="showOverview_">
+ <svg viewbox="0 0 128 128">
+ <line x1="19" y1="109" x2="49" y2="49"/>
+ <line x1="49" y1="49" x2="79" y2="79"/>
+ <line x1="79" y1="79" x2="109" y2="19"/>
+ </svg>
+ </span>
+
+ <span id="hide_overview" on-click="hideOverview_">
+ <svg viewbox="0 0 128 128">
+ <line x1="28" y1="28" x2="100" y2="100"/>
+ <line x1="28" y1="100" x2="100" y2="28"/>
+ </svg>
+ </span>
+
+ <span id="open_histograms" on-click="openHistograms_">
+ <svg viewbox="0 0 128 128">
+ <rect x="16" y="24" width="32" height="16"/>
+ <rect x="16" y="56" width="96" height="16"/>
+ <rect x="16" y="88" width="64" height="16"/>
+ </svg>
+ </span>
+
+ <span id="close_histograms" on-click="closeHistograms_">
+ <svg viewbox="0 0 128 128">
+ <line x1="28" y1="28" x2="100" y2="100"/>
+ <line x1="28" y1="100" x2="100" y2="28"/>
+ </svg>
+ </span>
+ </div>
+
+ <div id="overview_container">
+ </div>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ const NAME_COLUMN_WIDTH_PX = 300;
+
+ Polymer({
+ is: 'tr-v-ui-histogram-set-table-name-cell',
+
+ created() {
+ this.row_ = undefined;
+ this.overviewChart_ = undefined;
+ this.cellListener_ = this.onCellStateUpdate_.bind(this);
+ this.rootListener_ = this.onRootStateUpdate_.bind(this);
+ },
+
+ attached() {
+ if (this.row) {
+ this.row.rootViewState.addUpdateListener(this.rootListener_);
+ }
+ },
+
+ detached() {
+ this.row.rootViewState.removeUpdateListener(this.rootListener_);
+ // Don't need to removeUpdateListener for the row and cells; their
+ // lifetimes are the same as |this|.
+ },
+
+ get row() {
+ return this.row_;
+ },
+
+ build(row) {
+ if (this.row_ !== undefined) {
+ throw new Error('row must be set exactly once.');
+ }
+ this.row_ = row;
+ this.row.viewState.addUpdateListener(this.onRowStateUpdate_.bind(this));
+ this.constrainWidth = this.row.rootViewState.constrainNameColumn;
+ if (this.isAttached) {
+ this.row.rootViewState.addUpdateListener(this.rootListener_);
+ }
+
+ for (const cellState of this.row.viewState.cells.values()) {
+ cellState.addUpdateListener(this.cellListener_);
+ }
+
+ Polymer.dom(this.$.name).textContent = this.row.name;
+
+ this.title = this.row.name;
+ if (this.row.description) {
+ this.title += '\n' + this.row.description;
+ }
+
+ if (this.row.overviewDataRange.isEmpty ||
+ this.row.overviewDataRange.min === this.row.overviewDataRange.max) {
+ // TODO(#3744) Also hide this button when column or subrow units don't
+ // match.
+ this.$.show_overview.style.display = 'none';
+ }
+
+ let histogramCount = 0;
+ for (const cell of this.row.columns.values()) {
+ if (cell instanceof tr.v.Histogram &&
+ cell.numValues > 0) {
+ ++histogramCount;
+ }
+ }
+ if (histogramCount <= 1) {
+ this.$.open_histograms.style.display = 'none';
+ }
+ },
+
+ set constrainWidth(constrain) {
+ this.$.name.style.maxWidth = constrain ?
+ (this.nameWidthPx + 'px') : 'none';
+ },
+
+ get nameWidthPx() {
+ // tr-ui-b-table adds 16px of padding for each additional level of subRows
+ // nesting, so outer nameDivs can be wider than inner nameDivs.
+ return NAME_COLUMN_WIDTH_PX - (16 * this.row.depth);
+ },
+
+ get isOverflowing() {
+ return this.$.name.style.maxWidth !== 'none' &&
+ this.$.name.getBoundingClientRect().width === this.nameWidthPx;
+ },
+
+ get isOverviewed() {
+ return this.$.overview_container.style.display === 'block';
+ },
+
+ set isOverviewed(isOverviewed) {
+ if (isOverviewed === this.isOverviewed) return;
+ if (isOverviewed) {
+ this.showOverview_();
+ } else {
+ this.hideOverview_();
+ }
+ },
+
+ hideOverview_(opt_event) {
+ this.$.overview_container.style.display = 'none';
+ this.$.hide_overview.style.display = 'none';
+ this.$.show_overview.style.display = 'block';
+
+ if (opt_event !== undefined) {
+ opt_event.stopPropagation();
+ tr.b.Timing.instant('histogram-set-table-name-cell', 'hideOverview');
+ this.row.viewState.isOverviewed = this.isOverviewed;
+ }
+ },
+
+ showOverview_(opt_event) {
+ if (opt_event !== undefined) {
+ opt_event.stopPropagation();
+ tr.b.Timing.instant('histogram-set-table-name-cell', 'showOverview');
+ this.row.viewState.isOverviewed = true;
+ }
+
+ this.$.overview_container.style.display = 'block';
+ this.$.hide_overview.style.display = 'block';
+ this.$.show_overview.style.display = 'none';
+
+ if (this.overviewChart_ === undefined) {
+ const displayStatisticName =
+ this.row.rootViewState.displayStatisticName;
+ const data = [];
+ let unit;
+
+ for (const [displayLabel, hist] of this.row.sortedColumns()) {
+ if (!(hist instanceof tr.v.Histogram)) continue;
+
+ if (unit === undefined) {
+ unit = hist.unit;
+ } else if (unit !== hist.unit) {
+ // The columns have different units, so the overview chart cannot
+ // use a single unit to format all of the values, so don't display
+ // an overview chart at all.
+ data.splice(0);
+ break;
+ }
+
+ const statName = hist.getAvailableStatisticName(displayStatisticName);
+ const statScalar = hist.getStatisticScalar(statName);
+
+ if (statScalar !== undefined) {
+ data.push({
+ x: displayLabel,
+ y: statScalar.value,
+ });
+ }
+ }
+ if (data.length < 2) {
+ return;
+ }
+
+ this.overviewChart_ = new tr.ui.b.NameLineChart();
+ this.$.overview_container.appendChild(this.overviewChart_);
+ this.overviewChart_.displayXInHover = true;
+ this.overviewChart_.hideLegend = true;
+ this.overviewChart_.unit = unit;
+ this.overviewChart_.overrideDataRange = this.row.overviewDataRange;
+ this.overviewChart_.data = data;
+ }
+ },
+
+ openHistograms_(event) {
+ event.stopPropagation();
+ tr.b.Timing.instant('histogram-set-table-name-cell', 'openHistograms');
+ for (const cell of this.row.cells.values()) {
+ cell.isHistogramOpen = true;
+ }
+ this.$.close_histograms.style.display = 'block';
+ this.$.open_histograms.style.display = 'none';
+ },
+
+ closeHistograms_(event) {
+ event.stopPropagation();
+ tr.b.Timing.instant('histogram-set-table-name-cell', 'closeHistograms');
+ for (const cell of this.row.cells.values()) {
+ cell.isHistogramOpen = false;
+ }
+ this.$.open_histograms.style.display = 'block';
+ this.$.close_histograms.style.display = 'none';
+ },
+
+ onRootStateUpdate_(event) {
+ if (event.delta.constrainNameColumn) {
+ this.constrainWidth = this.row.rootViewState.constrainNameColumn;
+ }
+ if (this.row.viewState.isOverviewed &&
+ event.delta.displayStatisticName) {
+ this.row.resetOverviewDataRange();
+ if (this.overviewChart_ !== undefined) {
+ this.$.overview_container.removeChild(this.overviewChart_);
+ this.overviewChart_ = undefined;
+ }
+ this.showOverview_();
+ }
+ },
+
+ onRowStateUpdate_(event) {
+ if (event.delta.isOverviewed) {
+ this.isOverviewed = this.row.viewState.isOverviewed;
+ }
+ // This assumes that cell states are not updated.
+ },
+
+ onCellStateUpdate_(event) {
+ if (!event.delta.isOpen) return;
+
+ let cellCount = 0;
+ let openCellCount = 0;
+ for (const cell of this.row.cells.values()) {
+ if (!(cell.histogram instanceof tr.v.Histogram) ||
+ (cell.histogram.numValues === 0)) {
+ continue;
+ }
+ ++cellCount;
+ if (cell.isHistogramOpen) ++openCellCount;
+ }
+ if (cellCount <= 1) return;
+ const mostlyOpen = openCellCount > (cellCount / 2);
+ this.$.open_histograms.style.display = mostlyOpen ? 'none' : 'block';
+ this.$.close_histograms.style.display = mostlyOpen ? 'block' : 'none';
+ }
+ });
+
+ return {
+ NAME_COLUMN_WIDTH_PX,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html
new file mode 100644
index 00000000000..b4cb1a54020
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_row.html
@@ -0,0 +1,299 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/value/ui/histogram_set_table_cell.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_table_name_cell.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ class HistogramSetTableRow {
+ /**
+ * @param {!tr.v.HistogramSetHierarchy} hierarchy
+ * @param {!Element} baseTable tr-ui-b-table
+ * @param {!tr.v.ui.HistogramSetViewState} rootViewState
+ */
+ constructor(hierarchy, baseTable, rootViewState) {
+ this.hierarchy_ = hierarchy;
+ this.baseTable_ = baseTable;
+ this.rootViewState_ = rootViewState;
+ this.viewState_ = new tr.v.ui.HistogramSetTableRowState();
+ this.viewState_.addUpdateListener(this.onViewStateUpdate_.bind(this));
+ this.overviewDataRange_ = undefined;
+ this.nameCell_ = undefined;
+ this.cells_ = new Map();
+ this.subRows_ = [];
+
+ // Don't assign viewState.subRows or cells. There can't be anything
+ // listening to viewState, so avoid the overhead of dispatching an event.
+ for (const subHierarchy of hierarchy.subRows) {
+ const subRow = new HistogramSetTableRow(
+ subHierarchy, baseTable, rootViewState);
+ this.subRows_.push(subRow);
+ this.viewState.subRows.set(subRow.name, subRow.viewState);
+ }
+ for (const columnName of this.columns.keys()) {
+ this.viewState.cells.set(
+ columnName, new tr.v.ui.HistogramSetTableCellState());
+ }
+ }
+
+ /**
+ * @return {string}
+ */
+ get name() {
+ return this.hierarchy_.name;
+ }
+
+ /**
+ * @return {number}
+ */
+ get depth() {
+ return this.hierarchy_.depth;
+ }
+
+ /**
+ * @return {string}
+ */
+ get description() {
+ return this.hierarchy_.description;
+ }
+
+ /**
+ * @return {!Map.<string, !(undefined|tr.v.Histogram|tr.v.HistogramSet)>}
+ */
+ get columns() {
+ return this.hierarchy_.columns;
+ }
+
+ * sortedColumns() {
+ for (const col of this.baseTable_.tableColumns) {
+ yield [
+ col.displayLabel,
+ this.hierarchy_.columns.get(col.displayLabel),
+ ];
+ }
+ }
+
+ /**
+ * @return {!tr.b.Range}
+ */
+ get overviewDataRange() {
+ if (this.overviewDataRange_ === undefined) {
+ this.overviewDataRange_ = new tr.b.math.Range();
+
+ const displayStatisticName =
+ this.rootViewState.displayStatisticName;
+ const referenceDisplayLabel =
+ this.rootViewState.referenceDisplayLabel;
+
+ for (const [displayLabel, hist] of this.columns) {
+ if (hist instanceof tr.v.Histogram) {
+ const statName = hist.getAvailableStatisticName(
+ displayStatisticName);
+ const statScalar = hist.getStatisticScalar(statName);
+ if (statScalar !== undefined) {
+ this.overviewDataRange_.addValue(statScalar.value);
+ }
+ }
+
+ for (const subRow of this.subRows) {
+ const subHist = subRow.columns.get(displayLabel);
+ if (!(subHist instanceof tr.v.Histogram)) continue;
+
+ const refHist = subRow.columns.get(referenceDisplayLabel);
+ const statName = subHist.getAvailableStatisticName(
+ displayStatisticName, refHist);
+ const statScalar = subHist.getStatisticScalar(
+ statName, refHist);
+
+ if (statScalar !== undefined) {
+ this.overviewDataRange_.addValue(statScalar.value);
+ }
+ }
+ }
+ }
+ return this.overviewDataRange_;
+ }
+
+ /**
+ * overviewDataRange is used by histogram-set-table-cell (hstc) and
+ * histogram-set-table-name-cell (hstnc) to display overview line charts
+ * with consistent y-axes.
+ * overviewDataRange depends on HistogramSetViewState.displayStatisticName
+ * and referenceDisplayLabel, so it must be recomputed when either of those
+ * changes.
+ * overviewDataRange should not be recomputed for each hstc in the row; it
+ * should only be computed once when necessary, and cached.
+ * HistogramSetTableRow (HSTR) cannot listen to HistogramSetViewState
+ * (HSVS) updates because there is no way for it to remove the listener.
+ * However, Polymer has detached callbacks, so dom-modules can listen to
+ * HSVS updates without leaking memory.
+ * overviewDataRange should be recomputed only once whenever
+ * displayStatisticName or referenceDisplayLabel changes.
+ * There is exactly one hstnc per row.
+ * histogram-set-table-name-cell resets overviewDataRange when
+ * displayStatisticName or referenceDisplayLabel changes.
+ */
+ resetOverviewDataRange() {
+ this.overviewDataRange_ = undefined;
+ }
+
+ /**
+ * @return {!tr.v.ui.HistogramSetViewState}
+ */
+ get rootViewState() {
+ return this.rootViewState_;
+ }
+
+ /**
+ * @return {!Map.<string, !Element>} tr-v-ui-histogram-set-table-cell
+ */
+ get cells() {
+ return this.cells_;
+ }
+
+ /**
+ * @return {!Array.<tr.v.ui.HistogramSetTableRow>}
+ */
+ get subRows() {
+ return this.subRows_;
+ }
+
+ /**
+ * @return {!Array.<tr.v.ui.HistogramSetTableRowState>}
+ */
+ get viewState() {
+ return this.viewState_;
+ }
+
+ * walk() {
+ yield this;
+ for (const row of this.subRows) yield* row.walk();
+ }
+
+ static* walkAll(rootRows) {
+ for (const rootRow of rootRows) yield* rootRow.walk();
+ }
+
+ get nameCell() {
+ if (this.nameCell_ === undefined) {
+ this.nameCell_ = document.createElement(
+ 'tr-v-ui-histogram-set-table-name-cell');
+ this.nameCell_.build(this);
+ }
+ return this.nameCell_;
+ }
+
+ getCell(columnName) {
+ if (this.cells.has(columnName)) return this.cells.get(columnName);
+ const cell = document.createElement('tr-v-ui-histogram-set-table-cell');
+ cell.build(this, columnName, this.viewState.cells.get(columnName));
+ this.cells.set(columnName, cell);
+ return cell;
+ }
+
+ compareNames(other) {
+ return this.name.localeCompare(other.name);
+ }
+
+ compareCells(other, displayLabel) {
+ // If a reference column is selected, compare the absolute deltas
+ // between the two cells and their references.
+ const referenceDisplayLabel = this.rootViewState.referenceDisplayLabel;
+ let referenceCellA;
+ let referenceCellB;
+ if (referenceDisplayLabel &&
+ referenceDisplayLabel !== displayLabel) {
+ referenceCellA = this.columns.get(referenceDisplayLabel);
+ referenceCellB = other.columns.get(referenceDisplayLabel);
+ }
+
+ const cellA = this.columns.get(displayLabel);
+ let valueA = 0;
+ if (cellA instanceof tr.v.Histogram) {
+ const statisticA = cellA.getAvailableStatisticName(
+ this.rootViewState.displayStatisticName, referenceCellA);
+ const scalarA = cellA.getStatisticScalar(statisticA, referenceCellA);
+ if (scalarA) {
+ valueA = scalarA.value;
+ }
+ }
+
+ const cellB = other.columns.get(displayLabel);
+ let valueB = 0;
+ if (cellB instanceof tr.v.Histogram) {
+ const statisticB = cellB.getAvailableStatisticName(
+ this.rootViewState.displayStatisticName, referenceCellB);
+ const scalarB = cellB.getStatisticScalar(statisticB, referenceCellB);
+ if (scalarB) {
+ valueB = scalarB.value;
+ }
+ }
+
+ return valueA - valueB;
+ }
+
+ onViewStateUpdate_(event) {
+ if (event.delta.isExpanded) {
+ this.baseTable_.setExpandedForTableRow(this, this.viewState.isExpanded);
+ }
+
+ if (event.delta.subRows) {
+ throw new Error('HistogramSetTableRow.subRows must not be reassigned.');
+ }
+
+ if (event.delta.cells) {
+ // Only validate the cells that have already been built.
+ // Cells may not have been built yet, so only validate the cells that
+ // have been built.
+ for (const [displayLabel, cell] of this.cells) {
+ if (cell.viewState !== this.viewState.cells.get(displayLabel)) {
+ throw new Error('Only HistogramSetTableRow may update cells');
+ }
+ }
+ }
+ }
+
+ async restoreState(vs) {
+ // Don't use updateFromViewState() because it would overwrite cells and
+ // subRows, but we just want to restore them.
+ await this.viewState.update({
+ isExpanded: vs.isExpanded,
+ isOverviewed: vs.isOverviewed,
+ });
+
+ // If cells haven't been built yet, then their state will be restored when
+ // they are built.
+ for (const [displayLabel, cell] of this.cells) {
+ const previousState = vs.cells.get(displayLabel);
+ if (!previousState) continue;
+ await cell.viewState.updateFromViewState(previousState);
+ }
+ for (const row of this.subRows) {
+ const previousState = vs.subRows.get(row.name);
+ if (!previousState) continue;
+ await row.restoreState(previousState);
+ }
+ }
+
+ sortSubRows() {
+ const sortColumn = this.baseTable_.tableColumns[
+ this.rootViewState_.sortColumnIndex];
+ if (sortColumn === undefined) return;
+ this.subRows_.sort(sortColumn.cmp);
+ if (this.rootViewState_.sortDescending) {
+ this.subRows_.reverse();
+ }
+ }
+ }
+
+ return {
+ HistogramSetTableRow,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html
new file mode 100644
index 00000000000..f8d76afc72b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_table_test.html
@@ -0,0 +1,1679 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/assert_utils.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ // TODO(#3811) Clean up these tests.
+
+ const TEST_BOUNDARIES = tr.v.HistogramBinBoundaries.createLinear(0, 1e3, 20);
+
+ async function buildTable(test, histograms) {
+ // This should mirror HistogramImporter in order to be as similar to
+ // results.html as possible.
+ const table = document.createElement('tr-v-ui-histogram-set-table');
+
+ table.viewState = new tr.v.ui.HistogramSetViewState();
+ await table.viewState.update({
+ displayStatisticName: 'avg',
+ groupings: [tr.v.HistogramGrouping.HISTOGRAM_NAME],
+ });
+
+ table.style.display = 'none';
+ test.addHTMLOutput(table);
+
+ table.addEventListener('display-ready', () => {
+ table.style.display = '';
+ });
+
+ const collector = new tr.v.HistogramParameterCollector();
+ collector.process(histograms);
+
+ await table.build(
+ histograms,
+ histograms.sourceHistograms,
+ collector.labels,
+ async message => {
+ await tr.b.animationFrame();
+ });
+ return table;
+ }
+
+ function range(start, end) {
+ const result = [];
+ for (let i = start; i < end; ++i) result.push(i);
+ return result;
+ }
+
+ function getBaseTable(table) {
+ return tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.tagName === 'TR-UI-B-TABLE');
+ }
+
+ function getNameCells(table) {
+ return tr.ui.b.findDeepElementsMatchingPredicate(table, e =>
+ e.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-NAME-CELL');
+ }
+
+ function getTableCells(table) {
+ return tr.ui.b.findDeepElementsMatchingPredicate(table, e =>
+ e.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL');
+ }
+
+ test('viewSearchQuery', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2]);
+ const table = await buildTable(this, histograms);
+
+ await table.viewState.update({searchQuery: 'a'});
+ let cells = getTableCells(table);
+ assert.lengthOf(cells, 1);
+
+ await table.viewState.update({searchQuery: '[z-'});
+ cells = getTableCells(table);
+ assert.lengthOf(cells, 2);
+
+ await table.viewState.update({searchQuery: 'x'});
+ cells = getTableCells(table);
+ assert.lengthOf(cells, 0);
+
+ await table.viewState.update({searchQuery: ''});
+ cells = getTableCells(table);
+ assert.lengthOf(cells, 2);
+ });
+
+ test('controlSearchQuery', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count,
+ {value: 1, diagnostics: {r: tr.v.d.Breakdown.fromEntries([['0', 1]])}});
+ const bHist = histograms.createHistogram('b', tr.b.Unit.byName.count, []);
+ const related = new tr.v.d.RelatedNameMap();
+ related.set('0', bHist.name);
+ aHist.diagnostics.set('r', related);
+ const table = await buildTable(this, histograms);
+ await table.viewState.tableRowStates.get('a').cells.get('Value').update(
+ {isOpen: true});
+ const link = tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.tagName === 'TR-UI-A-ANALYSIS-LINK');
+ link.click();
+ assert.strictEqual('^(b)$', table.viewState.searchQuery);
+ });
+
+ test('viewReferenceDisplayLabel', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1], {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])
+ ]]),
+ });
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2], {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])
+ ]]),
+ });
+ const table = await buildTable(this, histograms);
+ const baseTable = getBaseTable(table);
+ assert.isUndefined(baseTable.selectedTableColumnIndex);
+
+ await table.viewState.update({referenceDisplayLabel: 'A'});
+ assert.strictEqual(1, baseTable.selectedTableColumnIndex);
+
+ await table.viewState.update({referenceDisplayLabel: 'B'});
+ assert.strictEqual(2, baseTable.selectedTableColumnIndex);
+ });
+
+ test('viewDisplayStatisticName', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, range(0, 10), {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])
+ ]]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, range(10, 20), {
+ diagnostics: new Map([[
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])
+ ]]),
+ });
+ const table = await buildTable(this, histograms);
+ let scalarSpans = tr.ui.b.findDeepElementsMatchingPredicate(table, e =>
+ e.tagName === 'TR-V-UI-SCALAR-SPAN');
+ assert.lengthOf(scalarSpans, 2);
+ assert.strictEqual('5', scalarSpans[0].unit.format(scalarSpans[0].value));
+ assert.strictEqual('15', scalarSpans[1].unit.format(scalarSpans[1].value));
+
+ await table.viewState.update({displayStatisticName: 'std'});
+ scalarSpans = tr.ui.b.findDeepElementsMatchingPredicate(table, e =>
+ e.tagName === 'TR-V-UI-SCALAR-SPAN');
+ assert.lengthOf(scalarSpans, 2);
+ assert.strictEqual('3', scalarSpans[0].unit.format(scalarSpans[0].value));
+ assert.strictEqual('3', scalarSpans[1].unit.format(scalarSpans[1].value));
+ });
+
+ test('autoShowAll', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ const bHist = histograms.createHistogram('b', tr.b.Unit.byName.count, []);
+ const related = new tr.v.d.RelatedNameMap();
+ related.set('0', bHist.name);
+ aHist.diagnostics.set('r', related);
+ const table = await buildTable(this, histograms);
+
+ let cells = getNameCells(table);
+ assert.lengthOf(cells, 2);
+ assert.strictEqual('a', cells[0].row.name);
+
+ await table.viewState.update({searchQuery: 'b'});
+ assert.isTrue(table.viewState.showAll);
+ cells = getNameCells(table);
+ assert.lengthOf(cells, 1);
+ assert.strictEqual('b', cells[0].row.name);
+ });
+
+ test('viewShowAll', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ const bHist = histograms.createHistogram('b', tr.b.Unit.byName.count, []);
+ const related = new tr.v.d.RelatedNameMap();
+ related.set('0', bHist.name);
+ aHist.diagnostics.set('r', related);
+ const table = await buildTable(this, histograms);
+
+ let cells = getNameCells(table);
+ assert.lengthOf(cells, 2);
+ assert.strictEqual('a', cells[0].row.name);
+ assert.strictEqual('b', cells[1].row.name);
+
+ await table.viewState.update({showAll: false});
+ cells = getNameCells(table);
+ assert.lengthOf(cells, 1);
+ assert.strictEqual('a', cells[0].row.name);
+ });
+
+ test('viewSortColumnIndex', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2]);
+ const table = await buildTable(this, histograms);
+ const baseTable = getBaseTable(table);
+ assert.strictEqual(baseTable.sortColumnIndex, 0);
+ assert.isFalse(baseTable.sortDescending);
+
+ await table.viewState.update({sortColumnIndex: 1, sortDescending: true});
+ assert.isTrue(baseTable.sortDescending);
+ assert.strictEqual(baseTable.sortColumnIndex, 1);
+ });
+
+ test('controlSortColumnIndex', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2]);
+ const table = await buildTable(this, histograms);
+
+ assert.strictEqual(0, table.viewState.sortColumnIndex);
+
+ tr.ui.b.findDeepElementsMatchingPredicate(
+ table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[0].click();
+ assert.strictEqual(0, table.viewState.sortColumnIndex);
+
+ tr.ui.b.findDeepElementsMatchingPredicate(
+ table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[1].click();
+ assert.strictEqual(1, table.viewState.sortColumnIndex);
+ });
+
+ test('viewSortDescending', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2]);
+ const table = await buildTable(this, histograms);
+
+ await table.viewState.update({sortColumnIndex: 0});
+
+ await table.viewState.update({sortDescending: true});
+
+ await table.viewState.update({sortDescending: false});
+ });
+
+ test('controlSortDescending', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2]);
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({sortColumnIndex: 0});
+
+ assert.isFalse(table.viewState.sortDescending);
+
+ tr.ui.b.findDeepElementsMatchingPredicate(
+ table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[0].click();
+ assert.isTrue(table.viewState.sortDescending);
+
+ tr.ui.b.findDeepElementsMatchingPredicate(
+ table, e => e.tagName === 'TR-UI-B-TABLE-HEADER-CELL')[0].click();
+ assert.isFalse(table.viewState.sortDescending);
+ });
+
+ test('sortUndefinedStatistics', async function() {
+ // The 'avg' statistic Scalar of an empty histogram is undefined, so
+ // HistogramSetTableRow.compareCells must not throw when it encounters
+ // undefined Scalars.
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ histograms.createHistogram('b', tr.b.Unit.byName.count, []);
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({sortColumnIndex: 1});
+ });
+
+ test('sortByDeltaStatistic', async function() {
+ const histograms0 = new tr.v.HistogramSet();
+ const histograms1 = new tr.v.HistogramSet();
+ const a0Hist = histograms0.createHistogram(
+ 'a', tr.b.Unit.byName.count, [0]);
+ const b0Hist = histograms0.createHistogram(
+ 'b', tr.b.Unit.byName.count, [0]);
+ const c0Hist = histograms0.createHistogram(
+ 'c', tr.b.Unit.byName.count, [3]);
+ const a1Hist = histograms1.createHistogram(
+ 'a', tr.b.Unit.byName.count, [1]);
+ const b1Hist = histograms1.createHistogram(
+ 'b', tr.b.Unit.byName.count, [2]);
+ const c1Hist = histograms1.createHistogram(
+ 'c', tr.b.Unit.byName.count, [3]);
+ histograms0.addSharedDiagnosticToAllHistograms(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['L0']));
+ histograms0.addSharedDiagnosticToAllHistograms(
+ tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(0));
+ histograms1.addSharedDiagnosticToAllHistograms(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['L1']));
+ histograms1.addSharedDiagnosticToAllHistograms(
+ tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(1));
+
+ const table = await buildTable(this, new tr.v.HistogramSet(
+ Array.from(histograms0).concat(Array.from(histograms1))));
+ await table.viewState.update({
+ displayStatisticName: tr.v.DELTA + 'avg',
+ referenceDisplayLabel: 'L0',
+ sortColumnIndex: 2,
+ });
+ const nameCells = getNameCells(table);
+ assert.strictEqual('c', nameCells[0].row.name);
+ assert.strictEqual('a', nameCells[1].row.name);
+ assert.strictEqual('b', nameCells[2].row.name);
+ });
+
+ test('sortMissing', async function() {
+ // Missing cells should be treated as zero for sorting purposes. The
+ // comparator must not return undefined or NaN.
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['x'])],
+ ]),
+ });
+ histograms.createHistogram('b', tr.b.Unit.byName.count, [2], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['x'])],
+ ]),
+ });
+ // 'c','x' intentionally missing
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['y'])],
+ ]),
+ });
+ // 'b','y' intentionally missing
+ histograms.createHistogram('c', tr.b.Unit.byName.count, [-1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['y'])],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({sortColumnIndex: 2});
+ let cells = getNameCells(table);
+ assert.lengthOf(cells, 3);
+ assert.strictEqual('c', cells[0].row.name);
+ assert.strictEqual('b', cells[1].row.name);
+ assert.strictEqual('a', cells[2].row.name);
+ await table.viewState.update({sortDescending: true});
+ cells = getNameCells(table);
+ assert.lengthOf(cells, 3);
+ assert.strictEqual('a', cells[0].row.name);
+ assert.strictEqual('b', cells[1].row.name);
+ assert.strictEqual('c', cells[2].row.name);
+ await table.viewState.update({sortColumnIndex: 1});
+ cells = getNameCells(table);
+ assert.lengthOf(cells, 3);
+ assert.strictEqual('b', cells[0].row.name);
+ assert.strictEqual('a', cells[1].row.name);
+ assert.strictEqual('c', cells[2].row.name);
+ await table.viewState.update({sortDescending: false});
+ cells = getNameCells(table);
+ assert.lengthOf(cells, 3);
+ assert.strictEqual('c', cells[0].row.name);
+ assert.strictEqual('a', cells[1].row.name);
+ assert.strictEqual('b', cells[2].row.name);
+ });
+
+ test('viewConstrainNameColumn', async function() {
+ // TODO(#4321): Switch to using skipped instead once it works
+ return; // https://github.com/catapult-project/catapult/issues/4320
+ /* eslint-disable no-unreachable */
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a'.repeat(100), tr.b.Unit.byName.count, []);
+ const table = await buildTable(this, histograms);
+ const nameCell = tr.b.getOnlyElement(getNameCells(table));
+ assert.isTrue(nameCell.isOverflowing);
+ assert.isAbove(350, nameCell.getBoundingClientRect().width);
+ assert.isTrue(table.viewState.constrainNameColumn);
+ const dots = tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS);
+ assert.strictEqual('block', dots.style.display);
+
+ await table.viewState.update({constrainNameColumn: false});
+ assert.isFalse(nameCell.isOverflowing);
+ assert.isBelow(350, nameCell.getBoundingClientRect().width);
+
+ await table.viewState.update({constrainNameColumn: true});
+ assert.isTrue(nameCell.isOverflowing);
+ assert.isAbove(350, nameCell.getBoundingClientRect().width);
+ /* eslint-enable no-unreachable */
+ });
+
+ test('controlConstrainNameColumn', async function() {
+ // TODO(#4321): Switch to using skipped instead once it works
+ return; // https://github.com/catapult-project/catapult/issues/4320
+ /* eslint-disable no-unreachable */
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a'.repeat(100), tr.b.Unit.byName.count, []);
+ const table = await buildTable(this, histograms);
+ const nameCell = tr.b.getOnlyElement(getNameCells(table));
+ assert.isTrue(nameCell.isOverflowing);
+ assert.isAbove(350, nameCell.getBoundingClientRect().width);
+ assert.isTrue(table.viewState.constrainNameColumn);
+ const dots = tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS);
+ assert.strictEqual('block', dots.style.display);
+
+ tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS).click();
+ assert.isFalse(table.viewState.constrainNameColumn);
+ await tr.b.animationFrame();
+ assert.isFalse(nameCell.isOverflowing);
+ assert.isBelow(350, nameCell.getBoundingClientRect().width);
+
+ tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS).click();
+ assert.isTrue(table.viewState.constrainNameColumn);
+ await tr.b.animationFrame();
+ assert.isTrue(nameCell.isOverflowing);
+ assert.isAbove(350, nameCell.getBoundingClientRect().width);
+ /* eslint-enable no-unreachable */
+ });
+
+ test('viewRowExpanded', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ aHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['A']));
+ const bHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]);
+ bHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['B']));
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+ assert.lengthOf(getTableCells(table), 1);
+
+ await table.viewState.tableRowStates.get('a').update({isExpanded: true});
+ assert.lengthOf(getTableCells(table), 3);
+
+ await table.viewState.tableRowStates.get('a').update({isExpanded: false});
+ assert.lengthOf(getTableCells(table), 1);
+ });
+
+ test('controlRowExpanded', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const aHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ aHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['A']));
+ const bHist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]);
+ bHist.diagnostics.set(tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['B']));
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+ assert.isFalse(table.viewState.tableRowStates.get('a').isExpanded);
+
+ const nameCell = tr.b.getOnlyElement(getNameCells(table));
+ nameCell.click();
+ assert.isTrue(table.viewState.tableRowStates.get('a').isExpanded);
+
+ nameCell.click();
+ assert.isFalse(table.viewState.tableRowStates.get('a').isExpanded);
+ });
+
+ test('viewIsOverviewed', async function() {
+ const histograms = new tr.v.HistogramSet();
+ let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B']));
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+
+ const nameCells = getNameCells(table);
+ const cells = getTableCells(table);
+ assert.isFalse(nameCells[0].isOverviewed);
+
+ await table.viewState.tableRowStates.get('a').update({isOverviewed: true});
+ assert.isTrue(nameCells[0].isOverviewed);
+
+ await table.viewState.tableRowStates.get('a').update({isOverviewed: false});
+ assert.isFalse(nameCells[0].isOverviewed);
+ });
+
+ test('overviewSorted', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [4], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['D'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [2], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [3], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['C'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({
+ groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ],
+ sortColumnIndex: 0,
+ sortDescending: true,
+ });
+ await table.viewState.tableRowStates.get('a').update({isOverviewed: true});
+
+ let cells = getTableCells(table);
+ let chart = tr.ui.b.findDeepElementMatchingPredicate(cells[0], e =>
+ e.tagName === 'svg' && e.parentNode.id === 'overview_container');
+ assert.strictEqual('D', chart.data[0].x);
+ assert.strictEqual('C', chart.data[1].x);
+ assert.strictEqual('B', chart.data[2].x);
+ assert.strictEqual('A', chart.data[3].x);
+
+ await table.viewState.update({
+ sortDescending: false,
+ });
+ cells = getTableCells(table);
+ chart = tr.ui.b.findDeepElementMatchingPredicate(cells[0], e =>
+ e.tagName === 'svg' && e.parentNode.id === 'overview_container');
+ assert.strictEqual('A', chart.data[0].x);
+ assert.strictEqual('B', chart.data[1].x);
+ assert.strictEqual('C', chart.data[2].x);
+ assert.strictEqual('D', chart.data[3].x);
+ });
+
+ test('controlIsOverviewed', async function() {
+ const histograms = new tr.v.HistogramSet();
+ let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [2]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B']));
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']));
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+
+ assert.isFalse(table.viewState.tableRowStates.get('a').isOverviewed);
+
+ const nameCells = getNameCells(table);
+ tr.ui.b.findDeepElementMatchingPredicate(nameCells[0], e =>
+ e.id === 'show_overview').click();
+ assert.isTrue(table.viewState.tableRowStates.get('a').isOverviewed);
+
+ tr.ui.b.findDeepElementMatchingPredicate(nameCells[0], e =>
+ e.id === 'hide_overview').click();
+ assert.isFalse(table.viewState.tableRowStates.get('a').isOverviewed);
+ });
+
+ test('overviewStatistic', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1, 1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1, 1, 1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({
+ displayStatisticName: 'count',
+ groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ],
+ });
+ await table.viewState.tableRowStates.get('a').update({isOverviewed: true});
+
+ let charts = tr.ui.b.findDeepElementsMatchingPredicate(table, e =>
+ e.tagName === 'svg' && e.parentNode.id === 'overview_container');
+ assert.lengthOf(charts, 3);
+ assert.lengthOf(charts[0].data, 2);
+ assert.lengthOf(charts[1].data, 2);
+ assert.lengthOf(charts[2].data, 2);
+ assert.strictEqual(charts[0].data[0].y, 3);
+ assert.strictEqual(charts[0].data[1].y, 7);
+ assert.strictEqual(charts[1].data[0].y, 1);
+ assert.strictEqual(charts[1].data[1].y, 2);
+ assert.strictEqual(charts[2].data[0].y, 3);
+ assert.strictEqual(charts[2].data[1].y, 4);
+
+ await table.viewState.update({
+ displayStatisticName: tr.v.DELTA + 'count',
+ referenceDisplayLabel: 'A',
+ });
+ charts = tr.ui.b.findDeepElementsMatchingPredicate(table, e =>
+ e.tagName === 'svg' && e.parentNode.id === 'overview_container');
+ assert.lengthOf(charts, 3);
+ assert.lengthOf(charts[0].data, 2);
+ assert.lengthOf(charts[1].data, 2);
+ assert.lengthOf(charts[2].data, 2);
+ assert.strictEqual(charts[0].data[0].y, 3);
+ assert.strictEqual(charts[0].data[1].y, 7);
+ assert.strictEqual(charts[1].data[0].y, 1);
+ assert.strictEqual(charts[1].data[1].y, 2);
+ assert.strictEqual(charts[2].data[0].y, 2);
+ assert.strictEqual(charts[2].data[1].y, 2);
+ });
+
+ test('overviewUnits', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1, 1, 1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['X'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ ]),
+ });
+ histograms.createHistogram(
+ 'a', tr.b.Unit.byName.unitlessNumber, [1, 1, 1, 1], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['Y'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({
+ displayStatisticName: 'count',
+ groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ],
+ });
+ await table.viewState.tableRowStates.get('a').update({
+ isExpanded: true,
+ isOverviewed: true,
+ });
+ await table.viewState.tableRowStates.get('a').subRows.get('X').update({
+ isOverviewed: true,
+ });
+
+ const nameCells = getNameCells(table);
+
+ // Check there is no overviewChart in name-cell when column units mismatch.
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(nameCells[0],
+ '#overview_container svg'));
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(nameCells[2],
+ '#overview_container svg'));
+
+ // When column units match, the overview chart should exist and have the
+ // correct unit.
+ const nameOverviewChart = tr.ui.b.findDeepElementMatching(nameCells[1],
+ '#overview_container svg');
+ assert.isDefined(nameOverviewChart);
+ assert.strictEqual(nameOverviewChart.unit, tr.b.Unit.byName.count);
+
+ const cells = getTableCells(table);
+
+ // When subrow units match, the overview chart should exist and have the
+ // correct unit.
+ const overviewChart = tr.ui.b.findDeepElementMatching(cells[0],
+ '#overview_container svg');
+ assert.isDefined(overviewChart);
+ assert.strictEqual(overviewChart.unit, tr.b.Unit.byName.count);
+
+ // Check there is no overviewChart in table-cell when subrow units mismatch.
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[1],
+ '#overview_container svg'));
+
+ // Check there is no overviewChart in table-cell when there are no subrows.
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[2],
+ '#overview_container svg'));
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[3],
+ '#overview_container svg'));
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[4],
+ '#overview_container svg'));
+ assert.isUndefined(tr.ui.b.findDeepElementMatching(cells[5],
+ '#overview_container svg'));
+ });
+
+ test('viewCellOpen', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ const table = await buildTable(this, histograms);
+ const cell = tr.b.getOnlyElement(getTableCells(table));
+ assert.isFalse(cell.isHistogramOpen);
+
+ await table.viewState.tableRowStates.get('a').cells.get('Value').update(
+ {isOpen: true});
+ assert.isTrue(cell.isHistogramOpen);
+
+ await table.viewState.tableRowStates.get('a').cells.get('Value').update(
+ {isOpen: false});
+ assert.isFalse(cell.isHistogramOpen);
+ });
+
+ test('controlCellOpen', async function() {
+ const histograms = new tr.v.HistogramSet();
+ let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, [1]);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B']));
+ const table = await buildTable(this, histograms);
+
+ assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('A')
+ .isOpen);
+ const cells = getTableCells(table);
+
+ tr.ui.b.findDeepElementMatchingPredicate(cells[0], e =>
+ e.tagName === 'TR-V-UI-SCALAR-SPAN').click();
+ assert.isTrue(table.viewState.tableRowStates.get('a').cells.get('A')
+ .isOpen);
+
+ tr.ui.b.findDeepElementMatchingPredicate(cells[0], e =>
+ e.id === 'close_histogram').click();
+ assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('A')
+ .isOpen);
+
+ tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.id === 'open_histograms').click();
+ assert.isTrue(table.viewState.tableRowStates.get('a').cells.get('A')
+ .isOpen);
+ assert.isTrue(table.viewState.tableRowStates.get('a').cells.get('B')
+ .isOpen);
+
+ tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.id === 'close_histograms').click();
+ assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('A')
+ .isOpen);
+ assert.isFalse(table.viewState.tableRowStates.get('a').cells.get('B')
+ .isOpen);
+ });
+
+ test('rebin', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a', tr.b.Unit.byName.count, range(0, 100), {
+ binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ ]),
+ });
+ histograms.createHistogram('a', tr.b.Unit.byName.count, range(50, 150), {
+ binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+
+ const cells = getTableCells(table);
+ assert.lengthOf(cells, 2);
+ assert.lengthOf(cells[0].histogram.allBins,
+ 2 + tr.v.DEFAULT_REBINNED_COUNT);
+ assert.lengthOf(cells[1].histogram.allBins,
+ 2 + tr.v.DEFAULT_REBINNED_COUNT);
+ assert.strictEqual(cells[0].histogram.allBins[0].range.max, 0);
+ assert.strictEqual(cells[1].histogram.allBins[0].range.max, 0);
+ assert.strictEqual(cells[0].histogram.allBins[41].range.min, 200);
+ assert.strictEqual(cells[1].histogram.allBins[41].range.min, 200);
+ });
+
+ test('leafHistograms', async function() {
+ const histograms = new tr.v.HistogramSet();
+ let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A']));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B']));
+ const table = await buildTable(this, histograms);
+ assert.lengthOf(table.leafHistograms, 1);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+ assert.lengthOf(table.leafHistograms, 2);
+ });
+
+ test('nameCellOverflow', async function() {
+ // TODO(#4321): Switch to using skipped instead once it works
+ return; // https://github.com/catapult-project/catapult/issues/4320
+ /* eslint-disable no-unreachable */
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('a'.repeat(100), tr.b.Unit.byName.count, []);
+ const table = await buildTable(this, histograms);
+ const nameCell = tr.b.getOnlyElement(getNameCells(table));
+ assert.isTrue(nameCell.isOverflowing);
+ assert.isAbove(350, nameCell.getBoundingClientRect().width);
+
+ const dots = tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS);
+ assert.strictEqual('block', dots.style.display);
+ dots.click();
+
+ await tr.b.animationFrame();
+ assert.isFalse(nameCell.isOverflowing);
+ assert.isBelow(350, nameCell.getBoundingClientRect().width);
+ /* eslint-enable no-unreachable */
+ });
+
+ test('nameCellOverflowOnExpand', async function() {
+ // TODO(#4321): Switch to using skipped instead once it works
+ return; // https://github.com/catapult-project/catapult/issues/4320
+ /* eslint-disable no-unreachable */
+ const histograms = new tr.v.HistogramSet();
+ let hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['0'.repeat(100)]));
+ hist = histograms.createHistogram('a', tr.b.Unit.byName.count, []);
+ hist.diagnostics.set(
+ tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet(['1'.repeat(100)]));
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+
+ const dots = tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.textContent === tr.v.ui.MIDLINE_HORIZONTAL_ELLIPSIS);
+ assert.strictEqual('none', dots.style.display);
+
+ const baseTable = getBaseTable(table);
+ await table.viewState.tableRowStates.get('a').update({isExpanded: true});
+
+ const nameCell = tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-NAME-CELL' &&
+ e.row.name === '0'.repeat(100));
+ await tr.b.animationFrame();
+ assert.isTrue(nameCell.isOverflowing);
+ assert.isAbove(350, nameCell.getBoundingClientRect().width);
+
+ assert.strictEqual('block', dots.style.display);
+ dots.click();
+
+ await tr.b.animationFrame();
+ assert.isFalse(nameCell.isOverflowing);
+ assert.isBelow(350, nameCell.getBoundingClientRect().width);
+ /* eslint-enable no-unreachable */
+ });
+
+ test('overviewCharts', async function() {
+ const binBoundaries = tr.v.HistogramBinBoundaries.createLinear(0, 150, 10);
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('foo', tr.b.Unit.byName.count, [0], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])],
+ ]),
+ });
+ histograms.createHistogram('foo', tr.b.Unit.byName.count, [10], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])],
+ ]),
+ });
+ histograms.createHistogram('foo', tr.b.Unit.byName.count, [100], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])],
+ ]),
+ });
+ histograms.createHistogram('foo', tr.b.Unit.byName.count, [110], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])],
+ ]),
+ });
+ histograms.createHistogram('bar', tr.b.Unit.byName.count, [0], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])],
+ ]),
+ });
+ histograms.createHistogram('bar', tr.b.Unit.byName.count, [9], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story0'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])],
+ ]),
+ });
+ histograms.createHistogram('bar', tr.b.Unit.byName.count, [90], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['0'])],
+ ]),
+ });
+ histograms.createHistogram('bar', tr.b.Unit.byName.count, [99], {
+ binBoundaries,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['story1'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['1'])],
+ ]),
+ });
+ const now = new Date().getTime();
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ ]});
+
+ for (const row of tr.v.ui.HistogramSetTableRowState.walkAll(
+ table.viewState.tableRowStates.values())) {
+ await row.update({isOverviewed: true});
+ }
+
+ let charts = tr.ui.b.findDeepElementsMatchingPredicate(
+ table, e => ((e.id === 'overview_container') &&
+ (e.style.display !== 'none')));
+ charts = charts.map(div => div.children[0]);
+ assert.lengthOf(charts, 6);
+
+ assert.deepEqual(JSON.stringify(charts[0].data),
+ JSON.stringify([{x: '0', y: 45}, {x: '1', y: 54}]));
+ tr.b.assertRangeEquals(
+ charts[0].dataRange, tr.b.math.Range.fromExplicitRange(0, 99));
+
+ assert.deepEqual(
+ charts[1].data, [{x: 'story0', y: 0}, {x: 'story1', y: 90}]);
+ tr.b.assertRangeEquals(
+ charts[1].dataRange, tr.b.math.Range.fromExplicitRange(0, 99));
+
+ assert.deepEqual(
+ charts[2].data, [{x: 'story0', y: 9}, {x: 'story1', y: 99}]);
+ tr.b.assertRangeEquals(
+ charts[2].dataRange, tr.b.math.Range.fromExplicitRange(0, 99));
+
+ assert.deepEqual(charts[3].data, [{x: '0', y: 50}, {x: '1', y: 60}]);
+ tr.b.assertRangeEquals(
+ charts[3].dataRange, tr.b.math.Range.fromExplicitRange(0, 110));
+
+ assert.deepEqual(
+ charts[4].data, [{x: 'story0', y: 0}, {x: 'story1', y: 100}]);
+ tr.b.assertRangeEquals(
+ charts[4].dataRange, tr.b.math.Range.fromExplicitRange(0, 110));
+
+ assert.deepEqual(
+ charts[5].data, [{x: 'story0', y: 10}, {x: 'story1', y: 110}]);
+ tr.b.assertRangeEquals(
+ charts[5].dataRange, tr.b.math.Range.fromExplicitRange(0, 110));
+
+ for (const row of tr.v.ui.HistogramSetTableRowState.walkAll(
+ table.viewState.tableRowStates.values())) {
+ await row.update({isOverviewed: false});
+ }
+
+ charts = tr.ui.b.findDeepElementsMatchingPredicate(
+ table, e => ((e.id === 'overview_container') &&
+ (e.style.display !== 'none')));
+ assert.lengthOf(charts, 0);
+ });
+
+ test('sortByDisplayStatistic', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram(
+ 'bar', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [0, 10], {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+ histograms.createHistogram(
+ 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [5], {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({
+ sortColumnIndex: 1,
+ sortDescending: false,
+ displayStatisticName: 'min',
+ });
+
+ let nameCells = getNameCells(table);
+ assert.strictEqual(nameCells[0].row.name, 'bar');
+ assert.strictEqual(nameCells[1].row.name, 'foo');
+
+ await table.viewState.update({sortDescending: true});
+
+ nameCells = getNameCells(table);
+ assert.strictEqual(nameCells[0].row.name, 'foo');
+ assert.strictEqual(nameCells[1].row.name, 'bar');
+
+ await table.viewState.update({displayStatisticName: 'max'});
+
+ nameCells = getNameCells(table);
+ assert.strictEqual(nameCells[0].row.name, 'bar');
+ assert.strictEqual(nameCells[1].row.name, 'foo');
+
+ await table.viewState.update({sortDescending: false});
+
+ nameCells = getNameCells(table);
+ assert.strictEqual(nameCells[0].row.name, 'foo');
+ assert.strictEqual(nameCells[1].row.name, 'bar');
+ });
+
+ test('displayStatistic', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const now = new Date().getTime();
+ const barHist = histograms.createHistogram(
+ 'a', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1, 2, 3], {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['bar'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)],
+ ]),
+ });
+ const fooHist = histograms.createHistogram(
+ 'a', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [10, 20, 30], {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['foo'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)],
+ ]),
+ });
+
+ // Add a Histogram with another name so that the table displays the scalars.
+ const quxHist = histograms.createHistogram(
+ 'qux', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['foo'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)],
+ ]),
+ });
+
+ const table = await buildTable(this, histograms);
+
+ function histogramsEqual(a, b) {
+ // This is not an exhaustive equality check. This only tests the fields
+ // that are distinguishing for this test().
+ if (a.name !== b.name) return false;
+ return tr.v.HistogramGrouping.DISPLAY_LABEL.callback(a) ===
+ tr.v.HistogramGrouping.DISPLAY_LABEL.callback(b);
+ }
+
+ let fooCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ elem.histogram &&
+ histogramsEqual(elem.histogram, fooHist)));
+ assert.isDefined(fooCell);
+
+ let fooContent = tr.ui.b.findDeepElementMatchingPredicate(
+ fooCell, elem => elem.id === 'content');
+ assert.isDefined(fooContent);
+
+ let barCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ elem.histogram &&
+ histogramsEqual(elem.histogram, barHist)));
+ assert.isDefined(barCell);
+
+ let barContent = tr.ui.b.findDeepElementMatchingPredicate(
+ barCell, elem => elem.id === 'content');
+ assert.isDefined(barContent);
+
+ assert.strictEqual(table.viewState.displayStatisticName, 'avg');
+ assert.strictEqual('20.000 ms', fooContent.textContent);
+ assert.strictEqual('2.000 ms', barContent.textContent);
+
+ await table.viewState.update({referenceDisplayLabel: 'foo'});
+
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ elem.histogram &&
+ histogramsEqual(elem.histogram, fooHist)));
+ assert.isDefined(fooCell);
+
+ fooContent = tr.ui.b.findDeepElementMatchingPredicate(
+ fooCell, elem => elem.id === 'content');
+ assert.isDefined(fooContent);
+
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ elem.histogram &&
+ histogramsEqual(elem.histogram, barHist)));
+ assert.isDefined(barCell);
+
+ barContent = tr.ui.b.findDeepElementMatchingPredicate(
+ barCell, elem => elem.id === 'content');
+ assert.isDefined(barContent);
+
+ await table.viewState.update({displayStatisticName: `${tr.v.DELTA}avg`});
+ assert.strictEqual('20.000 ms', fooContent.textContent);
+ assert.strictEqual('-18.000 ms', barContent.textContent);
+
+ await table.viewState.update({displayStatisticName: `%${tr.v.DELTA}avg`});
+
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ elem.histogram &&
+ histogramsEqual(elem.histogram, fooHist)));
+ assert.isDefined(fooCell);
+
+ fooContent = tr.ui.b.findDeepElementMatchingPredicate(
+ fooCell, elem => elem.id === 'content');
+ assert.isDefined(fooContent);
+
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ elem.histogram &&
+ histogramsEqual(elem.histogram, barHist)));
+ assert.isDefined(barCell);
+
+ barContent = tr.ui.b.findDeepElementMatchingPredicate(
+ barCell, elem => elem.id === 'content');
+ assert.isDefined(barContent);
+
+ assert.strictEqual(table.viewState.displayStatisticName,
+ `%${tr.v.DELTA}avg`);
+ assert.strictEqual('20.000 ms', fooContent.textContent);
+ assert.strictEqual('-90.0%', barContent.textContent);
+ });
+
+ test('requestSelectionChange', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const barHist = histograms.createHistogram(
+ 'bar', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1], {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+
+ const fooHist = histograms.createHistogram(
+ 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, {
+ value: 1,
+ diagnostics: {
+ breakdown: tr.v.d.Breakdown.fromEntries([
+ ['bar', 1],
+ ['qux', 0],
+ ]),
+ },
+ }, {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+
+ const quxHist = histograms.createHistogram(
+ 'qux', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+ const breakdown = new tr.v.d.RelatedNameMap();
+ breakdown.set('bar', barHist.name);
+ breakdown.set('qux', quxHist.name);
+ fooHist.diagnostics.set('breakdown', breakdown);
+
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({showAll: false});
+
+ let fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isDefined(fooCell);
+
+ let barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isUndefined(barCell);
+
+ fooCell.isHistogramOpen = true;
+
+ const barLink = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => elem.tagName === 'TR-UI-A-ANALYSIS-LINK');
+ assert.isDefined(barLink);
+ barLink.click();
+
+ await tr.b.animationFrame();
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isDefined(barCell);
+
+ await table.viewState.update({searchQuery: ''});
+
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isDefined(fooCell);
+
+ fooCell.isHistogramOpen = true;
+
+ const selectAllLink = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-UI-A-ANALYSIS-LINK') &&
+ (elem.textContent === 'Select All')));
+ assert.isDefined(selectAllLink);
+ selectAllLink.click();
+
+ assert.strictEqual(table.viewState.searchQuery, '^(bar|qux)$');
+
+ await tr.b.animationFrame();
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isUndefined(fooCell);
+
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isDefined(barCell);
+
+ const quxCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'qux')));
+ assert.isDefined(quxCell);
+ });
+
+ test('search', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram(
+ 'bar', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1], {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+ histograms.createHistogram(
+ 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [1], {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+
+ const table = await buildTable(this, histograms);
+
+ let fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isDefined(fooCell);
+
+ let barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isDefined(barCell);
+
+ await table.viewState.update({searchQuery: 'bar'});
+
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isUndefined(fooCell);
+
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isDefined(barCell);
+
+ await table.viewState.update({searchQuery: 'foo'});
+
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isDefined(fooCell);
+
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isUndefined(barCell);
+
+ // As users type in regexes, some intermediate forms may be invalid regexes.
+ // When the search is an invalid regex, just ignore it.
+ await table.viewState.update({searchQuery: '[a-'});
+
+ fooCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'foo')));
+ assert.isDefined(fooCell);
+
+ barCell = tr.ui.b.findDeepElementMatchingPredicate(
+ table, elem => (
+ (elem.tagName === 'TR-V-UI-HISTOGRAM-SET-TABLE-CELL') &&
+ (elem.histogram.name === 'bar')));
+ assert.isDefined(barCell);
+ });
+
+ test('emptyAndMissing', async function() {
+ const now = new Date().getTime();
+ const histograms = new tr.v.HistogramSet();
+
+ const histA = histograms.createHistogram(
+ 'histogram A', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS,
+ new tr.v.d.GenericSet(['iteration A'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)],
+ ]),
+ });
+
+ const histB = histograms.createHistogram(
+ 'histogram B', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS,
+ new tr.v.d.GenericSet(['iteration B'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)],
+ ]),
+ });
+
+ const histC = histograms.createHistogram(
+ 'histogram A', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS,
+ new tr.v.d.GenericSet(['iteration B'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START, new tr.v.d.DateRange(now)],
+ ]),
+ });
+
+ const table = await buildTable(this, histograms);
+
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.textContent === '(empty)'));
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => e.textContent === '(missing)'));
+ });
+
+ test('instantiate_1x1', async function() {
+ const histograms = new tr.v.HistogramSet();
+ const hist = histograms.createHistogram(
+ 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ binBoundaries: TEST_BOUNDARIES,
+ });
+ const table = await buildTable(this, histograms);
+
+ const baseTable = getBaseTable(table);
+ assert.strictEqual(baseTable.tableRows.length, 1);
+
+ const cell = tr.ui.b.findDeepElementMatchingPredicate(table, elem =>
+ elem.tagName === 'TR-V-UI-SCALAR-SPAN');
+ cell.click();
+
+ const yAxisText = tr.ui.b.findDeepElementMatchingPredicate(table, e =>
+ e.tagName === 'text' && e.textContent === '<0.000 ms');
+ assert.isBelow(0, yAxisText.getBBox().width);
+ });
+
+ test('merge_unmergeable', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('foo', tr.b.Unit.byName.count, [], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['A'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['Value'])],
+ ]),
+ });
+ histograms.createHistogram('foo', tr.b.Unit.byName.count, [], {
+ binBoundaries: tr.v.HistogramBinBoundaries.createLinear(0, 1e3, 21),
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet(['B'])],
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['Value'])],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+ assert.strictEqual(table.viewState.tableRowStates.size, 1);
+ assert.instanceOf(tr.b.getOnlyElement(getTableCells(table)).histogram,
+ tr.v.HistogramSet);
+ });
+
+ test('instantiate_1x5', async function() {
+ const histograms = new tr.v.HistogramSet();
+
+ for (let i = 0; i < 5; ++i) {
+ histograms.createHistogram(
+ 'foo', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ binBoundaries: TEST_BOUNDARIES,
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['' + i])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(new Date().getTime())],
+ ]),
+ });
+ }
+ const table = await buildTable(this, histograms);
+ });
+
+ test('instantiate_2x2', async function() {
+ const histograms = new tr.v.HistogramSet();
+ histograms.createHistogram('foo', tr.b.Unit.byName.count,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(new Date().getTime())],
+ ]),
+ });
+ histograms.createHistogram('bar', tr.b.Unit.byName.count,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['A'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(new Date().getTime())],
+ ]),
+ });
+ histograms.createHistogram('foo', tr.b.Unit.byName.count,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(new Date().getTime())],
+ ]),
+ });
+ histograms.createHistogram('bar', tr.b.Unit.byName.count,
+ range(0, 100).map(i => Math.random() * 1e3), {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet(['B'])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(new Date().getTime())],
+ ]),
+ });
+ const table = await buildTable(this, histograms);
+ const baseTable = getBaseTable(table);
+
+ assert.lengthOf(baseTable.tableColumns, 3);
+ assert.strictEqual('Name',
+ baseTable.tableColumns[0].title.children[0].textContent);
+ assert.strictEqual('A',
+ baseTable.tableColumns[1].title.textContent);
+ assert.strictEqual('B',
+ baseTable.tableColumns[2].title.textContent);
+
+ await table.viewState.update({referenceDisplayLabel: 'A'});
+ baseTable.rebuild();
+ assert.strictEqual(1, baseTable.selectedTableColumnIndex);
+ let cells = getTableCells(table);
+ assert.strictEqual(cells[1].referenceHistogram, cells[0].histogram);
+ assert.strictEqual(cells[3].referenceHistogram, cells[2].histogram);
+
+ await table.viewState.update({referenceDisplayLabel: 'B'});
+ cells = getTableCells(table);
+ assert.strictEqual(2, baseTable.selectedTableColumnIndex);
+ assert.strictEqual(cells[0].referenceHistogram, cells[1].histogram);
+ assert.strictEqual(cells[2].referenceHistogram, cells[3].histogram);
+
+ // Test sorting by the reference column when the displayStatistic is a delta
+ // statistic.
+ await table.viewState.update({sortColumnIndex: 2});
+ let nameCell = getNameCells(table)[0];
+ const originalFirstRow = nameCell.row.name;
+ // This is either 'foo' or 'bar' depending on Math.random() above.
+
+ await table.viewState.update({
+ sortDescending: !table.viewState.sortDescending,
+ });
+ baseTable.rebuild();
+ nameCell = getNameCells(table)[0];
+ assert.notEqual(originalFirstRow, nameCell.row.name);
+ });
+
+ test('merged', async function() {
+ const histograms = new tr.v.HistogramSet();
+ // Add 2^8=256 Histograms, all with the same name, with different metadata.
+ const benchmarkNames = ['bm A', 'bm B'];
+ const storyGroupingKeys0 = ['A', 'B'];
+ const storyGroupingKeys1 = ['C', 'D'];
+ const storyNames = ['story A', 'story B'];
+ const starts = [1439708400000, 1439794800000];
+ const labels = ['label A', 'label B'];
+ const name = 'name '.repeat(20);
+ const unit = tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;
+
+ for (const benchmarkName of benchmarkNames) {
+ for (const storyGroupingKey0 of storyGroupingKeys0) {
+ for (const storyGroupingKey1 of storyGroupingKeys1) {
+ for (const storyName of storyNames) {
+ for (const startMs of starts) {
+ for (let storysetCounter = 0; storysetCounter < 2;
+ ++storysetCounter) {
+ for (const label of labels) {
+ const samples = range(0, 100).map(i => {
+ return {
+ value: Math.random() * 1e3,
+ diagnostics: {i: new tr.v.d.GenericSet([i])},
+ };
+ });
+ histograms.createHistogram(name, unit, samples, {
+ description: 'The best description.',
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.LABELS,
+ new tr.v.d.GenericSet([label])],
+ [tr.v.d.RESERVED_NAMES.STORYSET_REPEATS,
+ new tr.v.d.GenericSet([storysetCounter])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARKS,
+ new tr.v.d.GenericSet([benchmarkName])],
+ [tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(startMs)],
+ [tr.v.d.RESERVED_NAMES.STORIES,
+ new tr.v.d.GenericSet([storyName])],
+ [tr.v.d.RESERVED_NAMES.STORY_TAGS,
+ new tr.v.d.GenericSet([
+ `storyGroupingKey0:${storyGroupingKey0}`,
+ `storyGroupingKey1:${storyGroupingKey1}`,
+ ])],
+ ]),
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ histograms.buildGroupingsFromTags([tr.v.d.RESERVED_NAMES.STORY_TAGS]);
+
+ const table = await buildTable(this, histograms);
+ await table.viewState.update({groupings: [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.BENCHMARKS),
+ tr.v.HistogramGrouping.BY_KEY.get('storyGroupingKey0Tag'),
+ tr.v.HistogramGrouping.BY_KEY.get('storyGroupingKey1Tag'),
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES),
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.BENCHMARK_START),
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORYSET_REPEATS),
+ ]});
+ const baseTable = getBaseTable(table);
+
+ assert.lengthOf(baseTable.tableColumns, 3);
+ const nameHeaderCell = baseTable.tableColumns[0].title;
+ assert.strictEqual('Name', nameHeaderCell.children[0].textContent);
+ assert.strictEqual('label A', baseTable.tableColumns[1].title.textContent);
+ assert.strictEqual('label B', baseTable.tableColumns[2].title.textContent);
+
+ const nameCell = tr.b.getOnlyElement(getNameCells(table));
+ assert.closeTo(346, nameCell.getBoundingClientRect().width, 1);
+
+ nameHeaderCell.children[1].click();
+ // toggleNameColumnWidth_ does not await viewState.update()
+ await tr.b.animationFrame();
+ assert.isBelow(322, nameCell.getBoundingClientRect().width);
+
+ nameHeaderCell.children[1].click();
+ await tr.b.animationFrame();
+ assert.closeTo(346, nameCell.getBoundingClientRect().width, 1);
+
+ assert.lengthOf(baseTable.tableRows, 1);
+ assert.strictEqual(name, baseTable.tableRows[0].name);
+ assert.lengthOf(baseTable.tableRows[0].subRows, 2);
+
+ // assertions only report their arguments, which is not enough information
+ // to diagnose problems with nested structures like tableRows -- the path to
+ // the particular row is needed. This code would be a bit simpler if each
+ // row were given a named variable, but the path to each subRow would still
+ // need to be tracked in order to provide for diagnosing.
+ const subRowPath = [];
+ function getSubRow() {
+ let row = baseTable.tableRows[0];
+ for (const index of subRowPath) {
+ row = row.subRows[index];
+ }
+ return row;
+ }
+
+ for (let i = 0; i < benchmarkNames.length; ++i) {
+ subRowPath.push(i);
+ assert.lengthOf(getSubRow().subRows, 2, subRowPath);
+ assert.strictEqual(benchmarkNames[i], getSubRow().name, subRowPath);
+
+ for (let s = 0; s < storyGroupingKeys0.length; ++s) {
+ subRowPath.push(s);
+ assert.lengthOf(getSubRow().subRows, 2, subRowPath);
+ assert.strictEqual(storyGroupingKeys0[s], getSubRow().name, subRowPath);
+
+ for (let t = 0; t < storyGroupingKeys1.length; ++t) {
+ subRowPath.push(t);
+ assert.lengthOf(getSubRow().subRows, 2, subRowPath);
+ assert.strictEqual(storyGroupingKeys1[t], getSubRow().name,
+ subRowPath);
+
+ for (let j = 0; j < storyNames.length; ++j) {
+ subRowPath.push(j);
+ assert.lengthOf(getSubRow().subRows, 2, subRowPath);
+ assert.strictEqual(storyNames[j], getSubRow().name, subRowPath);
+
+ for (let k = 0; k < starts.length; ++k) {
+ subRowPath.push(k);
+ assert.lengthOf(getSubRow().subRows, 2, subRowPath);
+ assert.strictEqual(tr.b.formatDate(new Date(starts[k])),
+ getSubRow().name, subRowPath);
+
+ for (let l = 0; l < 2; ++l) {
+ subRowPath.push(l);
+ assert.lengthOf(getSubRow().subRows, 0, subRowPath);
+ assert.strictEqual('' + l, getSubRow().name, subRowPath);
+ subRowPath.pop();
+ }
+ subRowPath.pop();
+ }
+ subRowPath.pop();
+ }
+ subRowPath.pop();
+ }
+ subRowPath.pop();
+ }
+ subRowPath.pop();
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html
new file mode 100644
index 00000000000..18ba824811f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view.html
@@ -0,0 +1,210 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/ui/null_brushing_state_controller.html">
+<link rel="import" href="/tracing/value/csv_builder.html">
+<link rel="import" href="/tracing/value/histogram_parameter_collector.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_controls.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_table.html">
+<link rel="import" href="/tracing/value/ui/visualizations_data_container.html">
+
+<dom-module id="tr-v-ui-histogram-set-view">
+ <template>
+ <style>
+ :host {
+ font-family: sans-serif;
+ }
+
+ #zero {
+ color: red;
+ /* histogram-set-table is used by both metrics-side-panel and results.html.
+ * This font-size rule has no effect in results.html, but improves
+ * legibility in the metrics-side-panel, which sets font-size in order to
+ * make this table denser.
+ */
+ font-size: initial;
+ }
+
+ #container {
+ display: none;
+ }
+
+ #visualizations{
+ display: none;
+ }
+ </style>
+
+ <div id="zero">zero Histograms</div>
+
+ <div id="container">
+ <tr-v-ui-histogram-set-controls id="controls">
+ </tr-v-ui-histogram-set-controls>
+
+ <tr-v-ui-visualizations-data-container id="visualizations">
+ </tr-v-ui-visualizations-data-container>
+
+ <tr-v-ui-histogram-set-table id="table"></tr-v-ui-histogram-set-table>
+ </div>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ Polymer({
+ is: 'tr-v-ui-histogram-set-view',
+
+ listeners: {
+ export: 'onExport_',
+ loadVisualization: 'onLoadVisualization_'
+ },
+
+ created() {
+ this.brushingStateController_ = new tr.ui.NullBrushingStateController();
+ this.viewState_ = new tr.v.ui.HistogramSetViewState();
+ this.visualizationLoaded_ = false;
+ },
+
+ ready() {
+ this.$.table.viewState = this.viewState;
+ this.$.controls.viewState = this.viewState;
+ },
+
+ attached() {
+ this.brushingStateController.parentController =
+ tr.c.BrushingStateController.getControllerForElement(this.parentNode);
+ },
+
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ get viewState() {
+ return this.viewState_;
+ },
+
+ get histograms() {
+ return this.$.table.histograms;
+ },
+
+ /**
+ * @param {!tr.v.HistogramSet} histograms
+ * @param {!Object=} opt_options
+ * @param {function(string):!Promise=} opt_options.progressconst
+ * @param {string=} opt_options.helpHref
+ * @param {string=} opt_options.feedbackHref
+ */
+ async build(histograms, opt_options) {
+ const options = opt_options || {};
+ const progress = options.progress || (() => Promise.resolve());
+
+ if (options.helpHref) this.$.controls.helpHref = options.helpHref;
+ if (options.feedbackHref) {
+ this.$.controls.feedbackHref = options.feedbackHref;
+ }
+
+ if (histograms === undefined || histograms.length === 0) {
+ this.$.container.style.display = 'none';
+ this.$.zero.style.display = 'block';
+ this.style.display = 'block';
+ return;
+ }
+ this.$.zero.style.display = 'none';
+ this.$.container.style.display = 'block';
+ this.$.container.style.maxHeight = (window.innerHeight - 16) + 'px';
+
+ const buildMark = tr.b.Timing.mark('histogram-set-view', 'build');
+ await progress('Finding important Histograms...');
+ const sourceHistogramsMark = tr.b.Timing.mark(
+ 'histogram-set-view', 'sourceHistograms');
+ const sourceHistograms = histograms.sourceHistograms;
+ sourceHistogramsMark.end();
+ // Disable show_all if all values are sourceHistograms.
+ this.$.controls.showAllEnabled = (
+ sourceHistograms.length !== histograms.length);
+
+ await progress('Collecting parameters...');
+ const collectParametersMark = tr.b.Timing.mark(
+ 'histogram-set-view', 'collectParameters');
+ const parameterCollector = new tr.v.HistogramParameterCollector();
+ parameterCollector.process(histograms);
+ this.$.controls.baseStatisticNames = parameterCollector.statisticNames;
+ this.$.controls.possibleGroupings = parameterCollector.possibleGroupings;
+ const displayLabels = parameterCollector.labels;
+ this.$.controls.displayLabels = displayLabels;
+ collectParametersMark.end();
+
+ // Only show visualizer button if values are from rendering benchmarks
+ const hist = [...histograms][0];
+ const benchmarks = hist.diagnostics.get(tr.v.d.RESERVED_NAMES.BENCHMARKS);
+ let enable = false;
+ if (benchmarks !== undefined && benchmarks.length > 0) {
+ for (const benchmark of benchmarks) {
+ if (benchmark.includes('rendering')) {
+ enable = true;
+ break;
+ }
+ }
+ }
+ this.$.controls.enableVisualization = enable;
+
+ // Table.build() displays its own progress.
+ await this.$.table.build(
+ histograms, sourceHistograms, displayLabels, progress);
+
+ buildMark.end();
+ },
+
+ onExport_(event) {
+ const mark = tr.b.Timing.mark('histogram-set-view', 'export' +
+ (event.merged ? 'Merged' : 'Raw') + event.format.toUpperCase());
+
+ const histograms = event.merged ? this.$.table.leafHistograms :
+ this.histograms;
+
+ let blob;
+ if (event.format === 'csv') {
+ const csv = new tr.v.CSVBuilder(histograms);
+ csv.build();
+ blob = new window.Blob([csv.toString()], {type: 'text/csv'});
+ } else if (event.format === 'json') {
+ blob = new window.Blob([JSON.stringify(histograms.asDicts())],
+ {type: 'text/json'});
+ } else {
+ throw new Error(`Unable to export format "${event.format}"`);
+ }
+
+ const path = window.location.pathname.split('/');
+ const basename = path[path.length - 1].split('.')[0] || 'histograms';
+
+ const anchor = document.createElement('a');
+ anchor.download = `${basename}.${event.format}`;
+ anchor.href = window.URL.createObjectURL(blob);
+ anchor.click();
+ mark.end();
+ },
+
+ onLoadVisualization_(event) {
+ if (!this.visualizationLoaded_) { // Initial loading
+ this.$.visualizations.style.display = 'block';
+ this.$.visualizations.build(this.$.table.leafHistograms,
+ this.histograms);
+ this.visualizationLoaded_ = true;
+ } else if (this.$.visualizations.style.display === 'none') {
+ // Toggle to hide
+ this.$.visualizations.style.display = 'block';
+ } else {
+ this.$.visualizations.style.display = 'none';
+ }
+ },
+ });
+
+ return {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html
new file mode 100644
index 00000000000..ade4ef2a9c4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_state.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/view_state.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ class HistogramSetViewState extends tr.b.ViewState {
+ constructor() {
+ super();
+ this.define('searchQuery', '');
+ this.define('referenceDisplayLabel', '');
+ this.define('displayStatisticName', '');
+ this.define('showAll', true);
+ this.define('groupings', []);
+ this.define('sortColumnIndex', 0);
+ this.define('sortDescending', false);
+ this.define('constrainNameColumn', true);
+ this.define('tableRowStates', new Map());
+ this.define('alpha', 0.01);
+ }
+ }
+
+ tr.b.ViewState.register(HistogramSetViewState);
+
+ class HistogramSetTableRowState extends tr.b.ViewState {
+ constructor() {
+ super();
+ this.define('isExpanded', false);
+ this.define('isOverviewed', false);
+ this.define('cells', new Map());
+ this.define('subRows', new Map());
+ this.define('diagnosticsTab', '');
+ }
+
+ asCompactDict() {
+ const result = {};
+ if (this.isExpanded) result.e = '1';
+ if (this.isOverviewed) result.o = '1';
+ if (this.diagnosticsTab) result.d = this.diagnosticsTab;
+ const cells = {};
+ for (const [name, cell] of this.cells) {
+ const cellDict = cell.asCompactDict();
+ if (cellDict === undefined) continue;
+ cells[name] = cellDict;
+ }
+ if (Object.keys(cells).length > 0) result.c = cells;
+
+ const subRows = {};
+ for (const [name, row] of this.subRows) {
+ const rowDict = row.asCompactDict();
+ if (rowDict === undefined) continue;
+ subRows[name] = rowDict;
+ }
+ if (Object.keys(subRows).length > 0) result.r = subRows;
+
+ if (Object.keys(result).length === 0) return undefined;
+
+ return result;
+ }
+
+ async updateFromCompactDict(dict) {
+ await this.update({
+ isExpanded: dict.e === '1',
+ isOverviewed: dict.o === '1',
+ diagnosticsTab: dict.d || '',
+ });
+
+ for (const [name, cellDict] of Object.entries(dict.c || {})) {
+ const cell = this.cells.get(name);
+ if (cell === undefined) continue;
+ await cell.updateFromCompactDict(cellDict);
+ }
+
+ for (const [name, subRowDict] of Object.entries(dict.r || {})) {
+ const subRow = this.subRows.get(name);
+ if (subRow === undefined) continue;
+ await subRow.updateFromCompactDict(subRowDict);
+ }
+ }
+
+ * walk() {
+ yield this;
+ for (const row of this.subRows.values()) yield* row.walk();
+ }
+
+ static* walkAll(rootRows) {
+ for (const rootRow of rootRows) yield* rootRow.walk();
+ }
+ }
+
+ tr.b.ViewState.register(HistogramSetTableRowState);
+
+ class HistogramSetTableCellState extends tr.b.ViewState {
+ constructor() {
+ super();
+ this.define('isOpen', false);
+ this.define('brushedBinRange', new tr.b.math.Range());
+ this.define('mergeSampleDiagnostics', true);
+ }
+
+ asCompactDict() {
+ const result = {};
+ if (this.isOpen) result.o = '1';
+ if (!this.mergeSampleDiagnostics) result.m = '0';
+ if (!this.brushedBinRange.isEmpty) {
+ result.b = this.brushedBinRange.min + '_' + this.brushedBinRange.max;
+ }
+ if (Object.keys(result).length === 0) return undefined;
+ return result;
+ }
+
+ async updateFromCompactDict(dict) {
+ let binRange = this.brushedBinRange;
+ if (dict.b) {
+ let [bMin, bMax] = dict.b.split('_');
+ bMin = parseInt(bMin);
+ bMax = parseInt(bMax);
+ if (bMin !== binRange.min || bMax !== binRange.max) {
+ binRange = tr.b.math.Range.fromExplicitRange(bMin, bMax);
+ }
+ }
+ await this.update({
+ isOpen: dict.o === '1',
+ brushedBinRange: binRange,
+ mergeSampleDiagnostics: dict.m !== '0',
+ });
+ }
+ }
+
+ tr.b.ViewState.register(HistogramSetTableCellState);
+
+ return {
+ HistogramSetTableCellState,
+ HistogramSetTableRowState,
+ HistogramSetViewState,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html
new file mode 100644
index 00000000000..ede486c98d5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_set_view_test.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate0', async function() {
+ const view = document.createElement('tr-v-ui-histogram-set-view');
+ const histograms = new tr.v.HistogramSet();
+
+ const hist = new tr.v.Histogram('a', tr.b.Unit.byName.normalizedPercentage);
+ for (let i = 0; i < 1e2; ++i) {
+ hist.addSample(Math.random());
+ }
+ histograms.addHistogram(hist);
+
+ this.addHTMLOutput(view);
+ await view.build(histograms);
+
+ assert.strictEqual('none', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.textContent === 'zero Histograms')).display);
+ assert.strictEqual('block', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.id === 'container')).display);
+ });
+
+ test('implicitUndefinedHistogramSet', async function() {
+ const view = document.createElement('tr-v-ui-histogram-set-view');
+ this.addHTMLOutput(view);
+ assert.strictEqual('block', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.textContent === 'zero Histograms')).display);
+ assert.strictEqual('none', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.id === 'container')).display);
+ });
+
+ test('explicitUndefinedHistogramSet', async function() {
+ const view = document.createElement('tr-v-ui-histogram-set-view');
+ this.addHTMLOutput(view);
+ view.build(undefined);
+ assert.strictEqual('block', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.textContent === 'zero Histograms')).display);
+ assert.strictEqual('none', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.id === 'container')).display);
+ });
+
+ test('emptyHistogramSet', async function() {
+ const view = document.createElement('tr-v-ui-histogram-set-view');
+ this.addHTMLOutput(view);
+ view.build(new tr.v.HistogramSet());
+ assert.strictEqual('block', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.textContent === 'zero Histograms')).display);
+ assert.strictEqual('none', getComputedStyle(
+ tr.ui.b.findDeepElementMatchingPredicate(
+ view, e => e.id === 'container')).display);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html
new file mode 100644
index 00000000000..c0382f66b93
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span.html
@@ -0,0 +1,599 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/ui/base/box_chart.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/base/name_bar_chart.html">
+<link rel="import" href="/tracing/ui/base/tab_view.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_map_table.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view_state.html">
+<link rel="import" href="/tracing/value/ui/scalar_map_table.html">
+
+<dom-module id="tr-v-ui-histogram-span">
+ <template>
+ <style>
+ #container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ }
+ #chart {
+ flex-grow: 1;
+ display: none;
+ }
+ #drag_handle, #diagnostics_tab_templates {
+ display: none;
+ }
+ #chart svg {
+ display: block;
+ }
+ #stats_container {
+ overflow-y: auto;
+ }
+ </style>
+
+ <div id="container">
+ <div id="chart"></div>
+ <div id="stats_container">
+ <tr-v-ui-scalar-map-table id="stats"></tr-v-ui-scalar-map-table>
+ </div>
+ </div>
+ <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle>
+
+ <tr-ui-b-tab-view id="diagnostics"></tr-ui-b-tab-view>
+
+ <div id="diagnostics_tab_templates">
+ <tr-v-ui-diagnostic-map-table id="metric_diagnostics"></tr-v-ui-diagnostic-map-table>
+
+ <tr-v-ui-diagnostic-map-table id="metadata_diagnostics"></tr-v-ui-diagnostic-map-table>
+
+ <div id="sample_diagnostics_container">
+ <div id="merge_sample_diagnostics_container">
+ <input type="checkbox" id="merge_sample_diagnostics" checked on-change="updateDiagnostics_">
+ <label for="merge_sample_diagnostics">Merge Sample Diagnostics</label>
+ </div>
+ <tr-v-ui-diagnostic-map-table id="sample_diagnostics"></tr-v-ui-diagnostic-map-table>
+ </div>
+ </div>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ const DEFAULT_BAR_HEIGHT_PX = 5;
+ const TRUNCATE_BIN_MARGIN = 0.15;
+ const IGNORE_DELTA_STATISTICS_NAMES = [
+ `${tr.v.DELTA}min`,
+ `%${tr.v.DELTA}min`,
+ `${tr.v.DELTA}max`,
+ `%${tr.v.DELTA}max`,
+ `${tr.v.DELTA}sum`,
+ `%${tr.v.DELTA}sum`,
+ `${tr.v.DELTA}count`,
+ `%${tr.v.DELTA}count`,
+ ];
+
+ Polymer({
+ is: 'tr-v-ui-histogram-span',
+
+ created() {
+ this.viewStateListener_ = this.onViewStateUpdate_.bind(this);
+ this.viewState = new tr.v.ui.HistogramSetTableCellState();
+ this.rowStateListener_ = this.onRowStateUpdate_.bind(this);
+ this.rowState = new tr.v.ui.HistogramSetTableRowState();
+ this.rootStateListener_ = this.onRootStateUpdate_.bind(this);
+ this.rootState = new tr.v.ui.HistogramSetViewState();
+
+ this.histogram_ = undefined;
+ this.referenceHistogram_ = undefined;
+ this.graphWidth_ = undefined;
+ this.graphHeight_ = undefined;
+ this.mouseDownBin_ = undefined;
+ this.prevBrushedBinRange_ = new tr.b.math.Range();
+ this.anySampleDiagnostics_ = false;
+ this.canMergeSampleDiagnostics_ = true;
+ this.mwuResult_ = undefined;
+ },
+
+ get rowState() {
+ return this.rowState_;
+ },
+
+ set rowState(rs) {
+ if (this.rowState) {
+ this.rowState.removeUpdateListener(this.rowStateListener_);
+ }
+ this.rowState_ = rs;
+ this.rowState.addUpdateListener(this.rowStateListener_);
+ if (this.isAttached) this.updateContents_();
+ },
+
+ get viewState() {
+ return this.viewState_;
+ },
+
+ set viewState(vs) {
+ if (this.viewState) {
+ this.viewState.removeUpdateListener(this.viewStateListener_);
+ }
+ this.viewState_ = vs;
+ this.viewState.addUpdateListener(this.viewStateListener_);
+ if (this.isAttached) this.updateContents_();
+ },
+
+ get rootState() {
+ return this.rootState_;
+ },
+
+ set rootState(vs) {
+ if (this.rootState) {
+ this.rootState.removeUpdateListener(this.rootStateListener_);
+ }
+ this.rootState_ = vs;
+ this.rootState.addUpdateListener(this.rootStateListener_);
+ if (this.isAttached) this.updateContents_();
+ },
+
+ build(histogram, opt_referenceHistogram) {
+ this.histogram_ = histogram;
+ this.$.metric_diagnostics.histogram = histogram;
+ this.$.sample_diagnostics.histogram = histogram;
+ this.referenceHistogram_ = opt_referenceHistogram;
+
+ if (this.histogram.canCompare(this.referenceHistogram)) {
+ this.mwuResult_ = tr.b.math.Statistics.mwu(
+ this.histogram.sampleValues,
+ this.referenceHistogram.sampleValues,
+ this.rootState.alpha);
+ }
+
+ this.anySampleDiagnostics_ = false;
+ for (const bin of this.histogram.allBins) {
+ if (bin.diagnosticMaps.length > 0) {
+ this.anySampleDiagnostics_ = true;
+ break;
+ }
+ }
+
+ if (this.isAttached) this.updateContents_();
+ },
+
+ onViewStateUpdate_(event) {
+ if (event.delta.brushedBinRange) {
+ if (this.chart_ !== undefined) {
+ this.chart_.brushedRange = this.viewState.brushedBinRange;
+ }
+ this.updateDiagnostics_();
+ }
+
+ if (event.delta.mergeSampleDiagnostics &&
+ (this.viewState.mergeSampleDiagnostics !==
+ this.$.merge_sample_diagnostics.checked)) {
+ this.$.merge_sample_diagnostics.checked =
+ this.canMergeSampleDiagnostics &&
+ this.viewState.mergeSampleDiagnostics;
+ this.updateDiagnostics_();
+ }
+ },
+
+ updateSignificance_() {
+ if (!this.mwuResult_) return;
+ this.$.stats.setSignificanceForKey(
+ `${tr.v.DELTA}avg`, this.mwuResult_.significance);
+ },
+
+ onRootStateUpdate_(event) {
+ if (event.delta.alpha && this.mwuResult_) {
+ this.mwuResult_.compare(this.rootState.alpha);
+ this.updateSignificance_();
+ }
+ },
+
+ onRowStateUpdate_(event) {
+ if (event.delta.diagnosticsTab) {
+ if (this.rowState.diagnosticsTab ===
+ this.$.sample_diagnostics_container.tabLabel) {
+ this.updateDiagnostics_();
+ } else {
+ for (const tab of this.$.diagnostics.subViews) {
+ if (this.rowState.diagnosticsTab === tab.tabLabel) {
+ this.$.diagnostics.selectedSubView = tab;
+ break;
+ }
+ }
+ }
+ }
+ },
+
+ ready() {
+ this.$.metric_diagnostics.tabLabel = 'histogram diagnostics';
+ this.$.sample_diagnostics_container.tabLabel = 'sample diagnostics';
+ this.$.metadata_diagnostics.tabLabel = 'metadata';
+ this.$.metadata_diagnostics.isMetadata = true;
+ this.$.diagnostics.addEventListener(
+ 'selected-tab-change', this.onSelectedDiagnosticsChanged_.bind(this));
+ this.$.drag_handle.target = this.$.container;
+ this.$.drag_handle.addEventListener(
+ 'drag-handle-resize', this.onResize_.bind(this));
+ },
+
+ attached() {
+ if (this.histogram_ !== undefined) this.updateContents_();
+ },
+
+ get canMergeSampleDiagnostics() {
+ return this.canMergeSampleDiagnostics_;
+ },
+
+ set canMergeSampleDiagnostics(merge) {
+ this.canMergeSampleDiagnostics_ = merge;
+ if (!merge) this.viewState.mergeSampleDiagnostics = false;
+ this.$.merge_sample_diagnostics_container.style.display = (
+ merge ? '' : 'none');
+ },
+
+ onResize_(event) {
+ event.stopPropagation();
+ let heightPx = parseInt(this.$.container.style.height);
+ if (heightPx < this.defaultGraphHeight) {
+ heightPx = this.defaultGraphHeight;
+ this.$.container.style.height = this.defaultGraphHeight + 'px';
+ }
+ this.chart_.graphHeight = heightPx - (this.chart_.margin.top +
+ this.chart_.margin.bottom);
+ this.$.stats_container.style.maxHeight =
+ this.chart_.getBoundingClientRect().height + 'px';
+ },
+
+ /**
+ * Get the width in pixels of the widest bar in the bar chart, not the total
+ * bar chart svg tag, which includes margins containing axes and legend.
+ *
+ * @return {number}
+ */
+ get graphWidth() {
+ return this.graphWidth_ || this.defaultGraphWidth;
+ },
+
+ /**
+ * Set the width in pixels of the widest bar in the bar chart, not the total
+ * bar chart svg tag, which includes margins containing axes and legend.
+ *
+ * @param {number} width
+ */
+ set graphWidth(width) {
+ this.graphWidth_ = width;
+ },
+
+ /**
+ * Get the height in pixels of the bars in the bar chart, not the total
+ * bar chart svg tag, which includes margins containing axes and legend.
+ *
+ * @return {number}
+ */
+ get graphHeight() {
+ return this.graphHeight_ || this.defaultGraphHeight;
+ },
+
+ /**
+ * Set the height in pixels of the bars in the bar chart, not the total
+ * bar chart svg tag, which includes margins containing axes and legend.
+ *
+ * @param {number} height
+ */
+ set graphHeight(height) {
+ this.graphHeight_ = height;
+ },
+
+ /**
+ * Get the height in pixels of one bar in the bar chart.
+ *
+ * @return {number}
+ */
+ get barHeight() {
+ return this.chart_.barHeight;
+ },
+
+ /**
+ * Set the height in pixels of one bar in the bar chart.
+ *
+ * @param {number} px
+ */
+ set barHeight(px) {
+ this.graphHeight = this.computeChartHeight_(px);
+ },
+
+ computeChartHeight_(barHeightPx) {
+ return (this.chart_.margin.top +
+ this.chart_.margin.bottom +
+ (barHeightPx * this.histogram.allBins.length));
+ },
+
+ get defaultGraphHeight() {
+ if (this.histogram && this.histogram.allBins.length === 1) {
+ return 150;
+ }
+ return this.computeChartHeight_(DEFAULT_BAR_HEIGHT_PX);
+ },
+
+ get defaultGraphWidth() {
+ if (this.histogram.allBins.length === 1) {
+ return 100;
+ }
+ return 300;
+ },
+
+ get brushedBins() {
+ const bins = [];
+ if (this.histogram && !this.viewState.brushedBinRange.isEmpty) {
+ for (let i = this.viewState.brushedBinRange.min;
+ i < this.viewState.brushedBinRange.max; ++i) {
+ bins.push(this.histogram.allBins[i]);
+ }
+ }
+ return bins;
+ },
+
+ async updateBrushedRange_(binIndex) {
+ const brushedBinRange = new tr.b.math.Range();
+ brushedBinRange.addValue(tr.b.math.clamp(
+ this.mouseDownBinIndex_, 0, this.histogram.allBins.length - 1));
+ brushedBinRange.addValue(tr.b.math.clamp(
+ binIndex, 0, this.histogram.allBins.length - 1));
+ brushedBinRange.max += 1;
+ await this.viewState.update({brushedBinRange});
+ },
+
+ onMouseDown_(chartEvent) {
+ chartEvent.stopPropagation();
+ if (!this.histogram) return;
+ this.prevBrushedBinRange_ = this.viewState.brushedBinRange;
+ this.mouseDownBinIndex_ = chartEvent.y;
+ this.updateBrushedRange_(chartEvent.y);
+ },
+
+ onMouseMove_(chartEvent) {
+ chartEvent.stopPropagation();
+ if (!this.histogram) return;
+ this.updateBrushedRange_(chartEvent.y);
+ },
+
+ onMouseUp_(chartEvent) {
+ chartEvent.stopPropagation();
+ if (!this.histogram) return;
+ this.updateBrushedRange_(chartEvent.y);
+ if (this.prevBrushedBinRange_.range === 1 &&
+ this.viewState.brushedBinRange.range === 1 &&
+ (this.prevBrushedBinRange_.min ===
+ this.viewState.brushedBinRange.min)) {
+ tr.b.Timing.instant('histogram-span', 'clearBrushedBins');
+ this.viewState.update({brushedBinRange: new tr.b.math.Range()});
+ } else {
+ tr.b.Timing.instant('histogram-span', 'brushBins');
+ }
+ this.mouseDownBinIndex_ = undefined;
+ },
+
+ async onSelectedDiagnosticsChanged_() {
+ await this.rowState.update({
+ diagnosticsTab: this.$.diagnostics.selectedSubView.tabLabel,
+ });
+ if ((this.$.diagnostics.selectedSubView ===
+ this.$.sample_diagnostics_container) &&
+ this.histogram &&
+ this.viewState.brushedBinRange.isEmpty) {
+ // When the user selects the sample diagnostics tab, if they haven't
+ // already brushed any bins, then automatically brush all bins.
+ const brushedBinRange = tr.b.math.Range.fromExplicitRange(
+ 0, this.histogram.allBins.length);
+ await this.viewState.update({brushedBinRange});
+ this.updateDiagnostics_();
+ }
+ },
+
+ updateDiagnostics_() {
+ let maps = [];
+ for (const bin of this.brushedBins) {
+ for (const map of bin.diagnosticMaps) {
+ maps.push(map);
+ }
+ }
+
+ if (this.$.merge_sample_diagnostics.checked !==
+ this.viewState.mergeSampleDiagnostics) {
+ this.viewState.update({
+ mergeSampleDiagnostics: this.$.merge_sample_diagnostics.checked});
+ }
+
+ if (this.viewState.mergeSampleDiagnostics) {
+ const merged = new tr.v.d.DiagnosticMap();
+ for (const map of maps) {
+ merged.addDiagnostics(map);
+ }
+ maps = [merged];
+ }
+
+ const mark = tr.b.Timing.mark('histogram-span',
+ (this.viewState.mergeSampleDiagnostics ? 'merge' : 'split') +
+ 'SampleDiagnostics');
+ this.$.sample_diagnostics.diagnosticMaps = maps;
+ mark.end();
+
+ if (this.anySampleDiagnostics_) {
+ this.$.diagnostics.selectedSubView =
+ this.$.sample_diagnostics_container;
+ }
+ },
+
+ get histogram() {
+ return this.histogram_;
+ },
+
+ get referenceHistogram() {
+ return this.referenceHistogram_;
+ },
+
+ getDeltaScalars_(statNames, scalarMap) {
+ if (!this.histogram.canCompare(this.referenceHistogram)) return;
+
+ for (const deltaStatName of tr.v.Histogram.getDeltaStatisticsNames(
+ statNames)) {
+ if (IGNORE_DELTA_STATISTICS_NAMES.includes(deltaStatName)) continue;
+ const scalar = this.histogram.getStatisticScalar(
+ deltaStatName, this.referenceHistogram, this.mwuResult_);
+ if (scalar === undefined) continue;
+ scalarMap.set(deltaStatName, scalar);
+ }
+ },
+
+ set isYLogScale(logScale) {
+ this.chart_.isYLogScale = logScale;
+ },
+
+ async updateContents_() {
+ this.$.chart.style.display = 'none';
+ this.$.drag_handle.style.display = 'none';
+ this.$.container.style.justifyContent = '';
+
+ while (Polymer.dom(this.$.chart).lastChild) {
+ Polymer.dom(this.$.chart).removeChild(
+ Polymer.dom(this.$.chart).lastChild);
+ }
+
+ if (!this.histogram) return;
+ this.$.container.style.display = '';
+
+ const scalarMap = new Map();
+ this.getDeltaScalars_(this.histogram.statisticsNames, scalarMap);
+ for (const [name, scalar] of this.histogram.statisticsScalars) {
+ scalarMap.set(name, scalar);
+ }
+ this.$.stats.scalarMap = scalarMap;
+ this.updateSignificance_();
+
+ const metricDiagnosticMap = new tr.v.d.DiagnosticMap();
+ const metadataDiagnosticMap = new tr.v.d.DiagnosticMap();
+ for (const [key, diagnostic] of this.histogram.diagnostics) {
+ // Hide implementation details.
+ if (diagnostic instanceof tr.v.d.RelatedNameMap) continue;
+
+ if (tr.v.d.RESERVED_NAMES_SET.has(key)) {
+ metadataDiagnosticMap.set(key, diagnostic);
+ } else {
+ metricDiagnosticMap.set(key, diagnostic);
+ }
+ }
+
+ const diagnosticTabs = [];
+ if (metricDiagnosticMap.size) {
+ this.$.metric_diagnostics.diagnosticMaps = [metricDiagnosticMap];
+ diagnosticTabs.push(this.$.metric_diagnostics);
+ }
+ if (this.anySampleDiagnostics_) {
+ diagnosticTabs.push(this.$.sample_diagnostics_container);
+ }
+ if (metadataDiagnosticMap.size) {
+ this.$.metadata_diagnostics.diagnosticMaps = [metadataDiagnosticMap];
+ diagnosticTabs.push(this.$.metadata_diagnostics);
+ }
+ this.$.diagnostics.resetSubViews(diagnosticTabs);
+ this.$.diagnostics.set('tabsHidden', diagnosticTabs.length < 2);
+
+ if (this.histogram.numValues <= 1) {
+ await this.viewState.update({
+ brushedBinRange: tr.b.math.Range.fromExplicitRange(
+ 0, this.histogram.allBins.length)});
+ this.$.container.style.justifyContent = 'flex-end';
+ return;
+ }
+
+ this.$.chart.style.display = 'block';
+ this.$.drag_handle.style.display = 'block';
+
+ if (this.histogram.allBins.length === 1) {
+ if (this.histogram.min !== this.histogram.max) {
+ this.chart_ = new tr.ui.b.BoxChart();
+ Polymer.dom(this.$.chart).appendChild(this.chart_);
+ this.chart_.graphWidth = this.graphWidth;
+ this.chart_.graphHeight = this.graphHeight;
+ this.chart_.hideXAxis = true;
+ this.chart_.data = [
+ {
+ x: '',
+ color: 'blue',
+ percentile_0: this.histogram.running.min,
+ percentile_25: this.histogram.getApproximatePercentile(0.25),
+ percentile_50: this.histogram.getApproximatePercentile(0.5),
+ percentile_75: this.histogram.getApproximatePercentile(0.75),
+ percentile_100: this.histogram.running.max,
+ }
+ ];
+ }
+ this.$.stats_container.style.maxHeight =
+ this.chart_.getBoundingClientRect().height + 'px';
+ await this.viewState.update({
+ brushedBinRange: tr.b.math.Range.fromExplicitRange(
+ 0, this.histogram.allBins.length)});
+ return;
+ }
+
+ this.chart_ = new tr.ui.b.NameBarChart();
+ Polymer.dom(this.$.chart).appendChild(this.chart_);
+ this.chart_.graphWidth = this.graphWidth;
+ this.chart_.graphHeight = this.graphHeight;
+ this.chart_.addEventListener('item-mousedown',
+ this.onMouseDown_.bind(this));
+ this.chart_.addEventListener('item-mousemove',
+ this.onMouseMove_.bind(this));
+ this.chart_.addEventListener('item-mouseup',
+ this.onMouseUp_.bind(this));
+ this.chart_.hideLegend = true;
+ this.chart_.getDataSeries('y').color = 'blue';
+ this.chart_.xAxisLabel = '#';
+ this.chart_.brushedRange = this.viewState.brushedBinRange;
+ if (!this.viewState.brushedBinRange.isEmpty) {
+ this.updateDiagnostics_();
+ }
+
+ const chartData = [];
+ const binCounts = [];
+ for (const bin of this.histogram.allBins) {
+ let x = bin.range.min;
+ if (x === -Number.MAX_VALUE) {
+ x = '<' + new tr.b.Scalar(
+ this.histogram.unit, bin.range.max).toString();
+ } else {
+ x = new tr.b.Scalar(this.histogram.unit, x).toString();
+ }
+ chartData.push({x, y: bin.count});
+ binCounts.push(bin.count);
+ }
+
+ // If the largest 1 or 2 bins are more than twice as large as the next
+ // largest bin, then set the dataRange max to TRUNCATE_BIN_MARGIN% more
+ // than that next largest bin.
+ binCounts.sort((x, y) => y - x);
+ const dataRange = tr.b.math.Range.fromExplicitRange(0, binCounts[0]);
+ if (binCounts[1] > 0 && binCounts[0] > (binCounts[1] * 2)) {
+ dataRange.max = binCounts[1] * (1 + TRUNCATE_BIN_MARGIN);
+ }
+ if (binCounts[2] > 0 && binCounts[1] > (binCounts[2] * 2)) {
+ dataRange.max = binCounts[2] * (1 + TRUNCATE_BIN_MARGIN);
+ }
+ this.chart_.overrideDataRange = dataRange;
+
+ this.chart_.data = chartData;
+ this.$.stats_container.style.maxHeight =
+ this.chart_.getBoundingClientRect().height + 'px';
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html
new file mode 100644
index 00000000000..2ca360b3348
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/histogram_span_test.html
@@ -0,0 +1,300 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/assert_utils.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/ui/histogram_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber);
+ h.addSample(-1, {foo: new tr.v.d.GenericSet(['a'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(0, {foo: new tr.v.d.GenericSet(['c'])});
+ h.addSample(500, {foo: new tr.v.d.GenericSet(['c'])});
+ h.addSample(999, {foo: new tr.v.d.GenericSet(['d'])});
+ h.addSample(1000, {foo: new tr.v.d.GenericSet(['d'])});
+
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(h);
+ });
+
+ test('emptyHistogram', function() {
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber);
+
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(h);
+ });
+
+ test('viewBrushedBinRange', async function() {
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(tr.v.Histogram.create('a', tr.b.Unit.byName.count,
+ [0, 1, 2, 3, 4].map(value => {
+ return {value, diagnostics: new Map([
+ ['i', new tr.v.d.GenericSet([value])]])};
+ })));
+ assert.isTrue(span.viewState.brushedBinRange.isEmpty);
+
+ await span.viewState.update({
+ brushedBinRange: tr.b.math.Range.fromExplicitRange(5, 10),
+ });
+ const chart = tr.ui.b.findDeepElementMatchingPredicate(
+ span, e => e.tagName === 'svg');
+ assert.strictEqual(5, chart.brushedRange.min);
+ assert.strictEqual(10, chart.brushedRange.max);
+ });
+
+ test('controlBrushedBinRange', async function() {
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(tr.v.Histogram.create('a', tr.b.Unit.byName.count,
+ [0, 1, 2, 3, 4]));
+ assert.isTrue(span.viewState.brushedBinRange.isEmpty);
+
+ span.onMouseDown_({
+ stopPropagation: () => undefined,
+ y: 21,
+ });
+ span.onMouseUp_({
+ stopPropagation: () => undefined,
+ y: 0,
+ });
+ tr.b.assertRangeEquals(span.viewState.brushedBinRange,
+ tr.b.math.Range.fromExplicitRange(0, 22));
+ });
+
+ test('viewMergeSampleDiagnostics', async function() {
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ const samples = [];
+ for (let i = 0; i < 5; ++i) {
+ samples.push({
+ value: i,
+ diagnostics: {
+ breakdown: tr.v.d.Breakdown.fromDict({
+ values: {
+ a: 5 - i,
+ b: i + 5,
+ c: i,
+ },
+ }),
+ },
+ });
+ }
+ span.build(tr.v.Histogram.create('', tr.b.Unit.byName.count, samples));
+ await span.viewState.update({brushedBinRange:
+ tr.b.math.Range.fromExplicitRange(0, 10)});
+ const merge = tr.ui.b.findDeepElementMatchingPredicate(span, e =>
+ e.id === 'merge_sample_diagnostics');
+ assert.isTrue(merge.checked);
+
+ await span.viewState.update({mergeSampleDiagnostics: false});
+ assert.isFalse(merge.checked);
+
+ await span.viewState.update({mergeSampleDiagnostics: true});
+ assert.isTrue(merge.checked);
+ });
+
+ test('controlMergeSampleDiagnostics', async function() {
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ const samples = [];
+ for (let i = 0; i < 5; ++i) {
+ samples.push({
+ value: i,
+ diagnostics: {
+ breakdown: tr.v.d.Breakdown.fromDict({
+ values: {
+ a: 5 - i,
+ b: i + 5,
+ c: i,
+ },
+ }),
+ },
+ });
+ }
+ span.build(tr.v.Histogram.create('', tr.b.Unit.byName.count, samples));
+ await span.viewState.update({brushedBinRange:
+ tr.b.math.Range.fromExplicitRange(0, 10)});
+ const merge = tr.ui.b.findDeepElementMatchingPredicate(span, e =>
+ e.id === 'merge_sample_diagnostics');
+ assert.isTrue(merge.checked);
+
+ merge.click();
+ assert.isFalse(span.viewState.mergeSampleDiagnostics);
+
+ merge.click();
+ assert.isTrue(span.viewState.mergeSampleDiagnostics);
+ });
+
+ test('mergeSampleDiagnostics', async function() {
+ // Add several samples with sample diagnostics to a Histogram, brush all of
+ // the bins, test that the sample diagnostics are merged.
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.normalizedPercentage);
+ h.addSample(0.1, {foo: tr.v.d.Breakdown.fromDict({values: {a: 1, b: 2}})});
+ h.addSample(0.3, {foo: tr.v.d.Breakdown.fromDict({values: {a: 3, b: 4}})});
+ h.addSample(0.5, {foo: tr.v.d.Breakdown.fromDict({values: {a: 5, b: 6}})});
+ h.addSample(0.7, {foo: tr.v.d.Breakdown.fromDict({values: {a: 7, b: 8}})});
+ h.addSample(0.9, {foo: tr.v.d.Breakdown.fromDict({values: {a: 9, b: 10}})});
+
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(h);
+ await span.viewState.update({
+ brushedBinRange: tr.b.math.Range.fromExplicitRange(0, h.allBins.length)});
+ let breakdowns = tr.ui.b.findDeepElementsMatchingPredicate(
+ span, e => e.tagName === 'TR-V-UI-BREAKDOWN-SPAN');
+ assert.lengthOf(breakdowns, 1);
+
+ const merge = tr.ui.b.findDeepElementMatchingPredicate(
+ span, e => e.id === 'merge_sample_diagnostics');
+ merge.click();
+ breakdowns = tr.ui.b.findDeepElementsMatchingPredicate(
+ span, e => e.tagName === 'TR-V-UI-BREAKDOWN-SPAN');
+ assert.lengthOf(breakdowns, 5);
+ });
+
+ test('cannotMergeSampleDiagnostics', async function() {
+ // Add several samples with sample diagnostics to a Histogram, brush all of
+ // the bins, test that the sample diagnostics are not merged.
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.normalizedPercentage);
+ h.addSample(0.1, {foo: tr.v.d.Breakdown.fromDict({values: {a: 1, b: 2}})});
+ h.addSample(0.3, {foo: tr.v.d.Breakdown.fromDict({values: {a: 3, b: 4}})});
+ h.addSample(0.5, {foo: tr.v.d.Breakdown.fromDict({values: {a: 5, b: 6}})});
+ h.addSample(0.7, {foo: tr.v.d.Breakdown.fromDict({values: {a: 7, b: 8}})});
+ h.addSample(0.9, {foo: tr.v.d.Breakdown.fromDict({values: {a: 9, b: 10}})});
+
+ const span = document.createElement('tr-v-ui-histogram-span');
+ span.canMergeSampleDiagnostics = false;
+ this.addHTMLOutput(span);
+ span.build(h);
+ await span.viewState.update({
+ brushedBinRange: tr.b.math.Range.fromExplicitRange(0, h.allBins.length)});
+ const breakdowns = tr.ui.b.findDeepElementsMatchingPredicate(
+ span, e => e.tagName === 'TR-V-UI-BREAKDOWN-SPAN');
+ assert.lengthOf(breakdowns, 5);
+ });
+
+ test('singleSample', function() {
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber);
+ h.addSample(100, {
+ sample_diagnostic_0: new tr.v.d.GenericSet(['foo']),
+ sample_diagnostic_1: new tr.v.d.GenericSet(['bar']),
+ });
+ h.diagnostics.set('histogram diagnostic 0', new tr.v.d.GenericSet(['baz']));
+ h.diagnostics.set('histogram diagnostic 1', new tr.v.d.GenericSet(['qux']));
+
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(h);
+ });
+
+ test('nans', function() {
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber);
+ h.addSample(undefined, {foo: new tr.v.d.GenericSet(['b'])});
+ h.addSample(NaN, {foo: new tr.v.d.GenericSet(['c'])});
+ h.customizeSummaryOptions({nans: true});
+
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(h);
+ });
+
+ test('singleBin', function() {
+ const h = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber,
+ tr.v.HistogramBinBoundaries.SINGULAR);
+ h.addSample(0);
+ h.addSample(25);
+ h.addSample(100);
+ h.addSample(100);
+ h.addSample(25);
+ h.addSample(50);
+ h.addSample(75);
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(h);
+ });
+
+ test('referenceHistogram', function() {
+ const span = document.createElement('tr-v-ui-histogram-span');
+ span.build(tr.v.Histogram.create('', tr.b.Unit.byName.count, [1, 10, 100], {
+ binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR,
+ }), tr.v.Histogram.create('', tr.b.Unit.byName.count, [2, 20, 200], {
+ binBoundaries: tr.v.HistogramBinBoundaries.SINGULAR,
+ }));
+ this.addHTMLOutput(span);
+ });
+
+ test('breakdownUnit', async function() {
+ const root = new tr.v.Histogram('root', tr.b.Unit.byName.sizeInBytes);
+ const sampleBreakdown = new tr.v.d.Breakdown();
+ sampleBreakdown.set('x', 30 << 20);
+ sampleBreakdown.set('y', 70 << 20);
+ root.addSample(100 << 20, {sampleBreakdown});
+ const rhb = new tr.v.d.RelatedNameMap();
+ root.diagnostics.set('rhb', rhb);
+ const aHist = new tr.v.Histogram('a', tr.b.Unit.byName.sizeInBytes);
+ rhb.set('a', aHist.name);
+ aHist.addSample(10 << 20);
+ const bHist = new tr.v.Histogram('b', tr.b.Unit.byName.sizeInBytes);
+ rhb.set('b', bHist.name);
+ bHist.addSample(90 << 20);
+ const span = document.createElement('tr-v-ui-histogram-span');
+ this.addHTMLOutput(span);
+ span.build(root);
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ span, e => e.textContent === '100.0 MiB'));
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ span, e => e.textContent === '30.0 MiB'));
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ span, e => e.textContent === '70.0 MiB'));
+ });
+
+ test('diagnosticsTabs', async function() {
+ const span = document.createElement('tr-v-ui-histogram-span');
+ span.build(tr.v.Histogram.create(
+ '', tr.b.Unit.byName.count, [
+ {value: 1, diagnostics: new Map([
+ ['sample diagnostic', new tr.v.d.GenericSet(['value1'])],
+ ])},
+ {value: 10, diagnostics: new Map([
+ ['sample diagnostic', new tr.v.d.GenericSet(['value10'])],
+ ])},
+ ], {
+ diagnostics: new Map([
+ [tr.v.d.RESERVED_NAMES.BENCHMARKS, new tr.v.d.GenericSet([
+ 'system_health.common_desktop'])],
+ ]),
+ }));
+ this.addHTMLOutput(span);
+
+ const sample = tr.ui.b.findDeepElementMatching(
+ span, '#sample_diagnostics_container');
+ assert.strictEqual(span.rowState.diagnosticsTab, sample.tabLabel);
+ const tabs = tr.ui.b.findDeepElementMatching(
+ span, 'TR-UI-B-TAB-VIEW');
+ assert.strictEqual(tabs.selectedSubView, sample);
+
+ const metadata = tr.ui.b.findDeepElementMatching(
+ span, '#metadata_diagnostics');
+ await span.rowState.update({diagnosticsTab: metadata.tabLabel});
+ assert.strictEqual(tabs.selectedSubView, metadata);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html
new file mode 100644
index 00000000000..1bc6ea696b3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization.html
@@ -0,0 +1,353 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2018 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/base/math/running_statistics.html">
+<link rel="import" href="/tracing/ui/base/name_bar_chart.html">
+<link rel="import" href="/tracing/ui/base/name_column_chart.html">
+<dom-module id='tr-v-ui-metrics-visualization'>
+ <template>
+ <style>
+ button {
+ padding: 5px;
+ font-size: 14px;
+ }
+
+ .text_input {
+ width: 50px;
+ padding: 4px;
+ font-size: 14px;
+ }
+
+ .error {
+ color: red;
+ display: none;
+ }
+
+ .container {
+ position: relative;
+ display: inline-block;
+ margin-left: 15px;
+ }
+
+ #title {
+ font-size: 20px;
+ font-weight: bold;
+ padding-bottom: 5px;
+ }
+
+ #selectors {
+ display: block;
+ padding-bottom: 10px;
+ }
+
+ #search_page {
+ width: 200px;
+ margin-left: 30px;
+ }
+
+ #close {
+ display: none;
+ vertical-align: top;
+ }
+
+ #close svg{
+ height: 1em;
+ }
+
+ #close svg line {
+ stroke-width: 18;
+ stroke: black;
+ }
+
+ #close:hover svg {
+ background: black;
+ }
+
+ #close:hover svg line {
+ stroke: white;
+ }
+ </style>
+ <span id="aggregateContainer" class="container">
+ </span>
+ <span id="pageByPageContainer" class="container">
+ <span id="selectors">
+ <span id="percentile_label">Percentile Range:</span>
+ <input id="start" class="text_input" placeholder="0">
+ <input id="end" class="text_input" placeholder="100">
+ <button id="filter" on-tap="filterByPercentile_">Filter</button>
+ <input id="search_page" class="text_input" placeholder="Page Name">
+ <button id="search" on-tap="searchByPage_">Search</button>
+ <span id="search_error" class="error">Sorry, could not find that page!</span>
+ </span>
+ </span>
+ <div id="submetricsContainer" display="block">
+ <span id="close">
+ <svg viewbox="0 0 128 128">
+ <line x1="28" y1="28" x2="100" y2="100"/>
+ <line x1="28" y1="100" x2="100" y2="28"/>
+ </svg>
+ </span>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.v.ui', function() {
+ const PAGE_BREAKDOWN_KEY = 'pageBreakdown';
+
+ Polymer({
+ is: 'tr-v-ui-metrics-visualization',
+
+ created() {
+ this.charts_ = new Map();
+ },
+
+ ready() {
+ this.$.start.addEventListener ('keydown', (e) => {
+ if (e.key === 'Enter') this.filterByPercentile_();
+ });
+
+ this.$.end.addEventListener ('keydown', (e) => {
+ if (e.key === 'Enter') this.filterByPercentile_();
+ });
+
+ this.$.search_page.addEventListener ('keydown', (e) => {
+ if (e.key === 'Enter') this.searchByPage_();
+ });
+ },
+
+ build(chartData) {
+ this.title_ = chartData.title;
+ this.aggregateData_ = chartData.aggregate;
+ this.data_ = chartData.page;
+ this.submetricsData_ = chartData.submetrics;
+ this.benchmarkCount_ = chartData.aggregate.length;
+
+ // build aggregate chart
+ const aggregateChart = this.initializeColumnChart(this.title_);
+ Polymer.dom(this.$.aggregateContainer).appendChild(aggregateChart);
+ this.charts_.set(tr.v.ui.AGGREGATE_KEY, aggregateChart);
+ this.setChartColors_(tr.v.ui.AGGREGATE_KEY);
+ aggregateChart.data = chartData.aggregate;
+ this.setChartSize_(tr.v.ui.AGGREGATE_KEY);
+
+ // build page by page
+ const newChart = this.initializeColumnChart(this.title_ + ' Breakdown');
+ newChart.enableToolTip = true;
+ newChart.toolTipCallBack = (rect) =>
+ this.openChildChart_(rect);
+ Polymer.dom(this.$.pageByPageContainer).appendChild(newChart);
+ this.charts_.set(PAGE_BREAKDOWN_KEY, newChart);
+ this.setChartColors_(PAGE_BREAKDOWN_KEY);
+ newChart.data = this.data_;
+ this.setChartSize_(PAGE_BREAKDOWN_KEY);
+ },
+
+ setChartSize_(page) {
+ const chart = this.charts_.get(page);
+ const pageCount = chart.data.length;
+ chart.graphHeight = tr.b.math.clamp(pageCount * 20, 400, 600);
+ chart.graphWidth = tr.b.math.clamp(pageCount * 30, 200, 1000);
+ },
+
+ // Assign color gradient to series in chart
+ setChartColors_(page) {
+ const chart = this.charts_.get(page);
+ const metrics = tr.v.ui.METRICS.get(this.title_);
+ for (let i = 0; i < this.benchmarkCount_; ++i) {
+ for (let j = 0; j < metrics.length; ++j) {
+ const mainColorIndex = j % tr.v.ui.COLORS.length;
+ const subColorIndex = i % tr.v.ui.COLORS[mainColorIndex].length;
+ const color = tr.v.ui.COLORS[mainColorIndex][subColorIndex];
+ const series = metrics[j] + '-' + this.aggregateData_[i].x;
+ chart.getDataSeries(series).color = color;
+ if (i === 0) {
+ chart.getDataSeries(series).title = metrics[j];
+ } else {
+ chart.getDataSeries(series).title = '';
+ }
+ }
+ }
+ },
+
+ // Element creation
+ initializeColumnChart(title) {
+ const newChart = new tr.ui.b.NameColumnChart();
+ newChart.hideLegend = false;
+ newChart.isStacked = true;
+ newChart.yAxisLabel = 'ms';
+ newChart.hideXAxis = true;
+ newChart.displayXInHover = true;
+ newChart.isGrouped = true;
+ newChart.showTitleInLegend = true;
+ newChart.chartTitle = title;
+ newChart.titleHeight = '14pt';
+ return newChart;
+ },
+
+ initializeChildChart_(title, height, width) {
+ const div = document.createElement('div');
+ div.classList.add('container');
+ Polymer.dom(this.$.submetricsContainer).
+ insertBefore(div, this.$.submetricsContainer.firstChild);
+
+ const childChart = new tr.ui.b.NameBarChart();
+ childChart.xAxisLabel = 'ms';
+ childChart.chartTitle = title;
+ childChart.graphHeight = height;
+ childChart.graphWidth = width;
+ childChart.titleHeight = '14pt';
+ childChart.isStacked = true;
+ childChart.hideLegend = true;
+ childChart.isGrouped = true;
+ childChart.isWaterfall = true;
+
+ div.appendChild(childChart);
+
+ const button = this.initializeCloseButton_(div,
+ this.$.submetricsContainer);
+ div.appendChild(button);
+ return childChart;
+ },
+
+ initializeCloseButton_(div, parent) {
+ const button = this.$.close.cloneNode(true);
+ button.style.display = 'inline-block';
+ button.addEventListener ('click', () => {
+ Polymer.dom(parent).removeChild(div);
+ });
+ return button;
+ },
+
+ // Create child chart and populate it
+ openChildChart_(rect) {
+ // Find main metric and corresponding sub-metrics
+ const metrics = tr.v.ui.METRICS.get(this.title_);
+ let metric;
+ let metricIndex;
+ for (let i = 0; i < metrics.length; ++i) {
+ if (rect.key.startsWith(metrics[i])) {
+ metric = metrics[i];
+ metricIndex = i;
+ break;
+ }
+ }
+
+ // Create child chart
+ const page = rect.datum.group;
+ const title = this.title_ + ' ' + metric + ': ' + page;
+ const submetrics = this.submetricsData_.get(page).get(metric);
+ const width = tr.b.math.clamp(submetrics.size * 150, 300, 700);
+ const height = tr.b.math.clamp(submetrics.size *
+ this.benchmarkCount_ * 50, 300, 700);
+
+ const childChart = this.initializeChildChart_(title, height, width);
+
+ // Get breakdown data for main step
+ childChart.data = this.processSubmetrics_(childChart,
+ submetrics, 0, metricIndex).data;
+ },
+
+ processSubmetrics_(chart, submetrics, hideValue, metricIndex) {
+ const finalData = [];
+ let submetricIndex = 0;
+ for (const submetric of submetrics.values()) {
+ let benchmarkIndex = 0;
+ for (const benchmark of submetric.values()) {
+ benchmark.hide = !hideValue ? 0 : hideValue;
+ const series = benchmark.x + '-' + benchmark.group;
+ const mainColorIndex = metricIndex % tr.v.ui.COLORS.length;
+ const subColorIndex = benchmarkIndex %
+ tr.v.ui.COLORS[mainColorIndex].length;
+ chart.getDataSeries(series).color =
+ tr.v.ui.COLORS[mainColorIndex][subColorIndex];
+ if (benchmarkIndex === (this.benchmarkCount_ - 1)) {
+ hideValue += benchmark[series];
+ }
+ finalData.push(benchmark);
+ benchmarkIndex++;
+ }
+ submetricIndex++;
+ }
+ return {data: finalData, hide: hideValue};
+ },
+
+ // Handle filtering by start and end percentiles
+ filterByPercentile_() {
+ const startPercentile = this.$.start.value;
+ const endPercentile = this.$.end.value;
+
+ if (startPercentile === '' || endPercentile === '') return;
+
+ const length = this.data_.length / (this.benchmarkCount_ + 1);
+ const startIndex = this.getPercentileIndex_(startPercentile, length);
+ const endIndex = this.getPercentileIndex_(endPercentile, length);
+ this.charts_.get(PAGE_BREAKDOWN_KEY).data =
+ this.data_.slice(startIndex, endIndex);
+ },
+
+ // Get index of x percentile value
+ getPercentileIndex_(percentile, arrayLength) {
+ const index = Math.ceil(arrayLength * (percentile / 100.0));
+ if (index === -1) return 0;
+ if (index >= arrayLength) return arrayLength;
+ return index * this.benchmarkCount_;
+ },
+
+ // Handle searching by page name
+ searchByPage_() {
+ const criteria = this.$.search_page.value;
+ if (criteria === '') return;
+
+ const query = new RegExp(criteria);
+
+ const filteredData = [...this.data_]
+ .filter(group => {
+ if (group.group) return group.group.match(query);
+ return false;
+ });
+
+ if (filteredData.length < 1) {
+ this.$.search_error.style.display = 'block';
+ return;
+ }
+
+ // Create child chart with breakdown data
+ const page = filteredData[0].group;
+ const title = this.title_ + ' Breakdown: ' + page;
+ const metricToSubmetricMap = this.submetricsData_.get(page);
+
+ let totalSubmetrics = 0;
+ for (const submetrics of metricToSubmetricMap.values()) {
+ for (const benchmark of submetrics.values()) {
+ totalSubmetrics += benchmark.length;
+ }
+ }
+ const width = tr.b.math.clamp(totalSubmetrics * 150, 300, 700);
+ const height = tr.b.math.clamp(totalSubmetrics *
+ this.benchmarkCount_ * 30, 300, 700);
+
+ const childChart = this.initializeChildChart_(title, height, width);
+
+ const childData = [];
+ let hide = 0;
+ let metricIndex = 0;
+ for (const submetrics of metricToSubmetricMap.values()) {
+ const submetricsData = this.processSubmetrics_(childChart, submetrics,
+ hide, metricIndex);
+ childData.push(...submetricsData.data);
+ hide = submetricsData.hide;
+ metricIndex++;
+ }
+ childChart.data = childData;
+ },
+
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html
new file mode 100644
index 00000000000..0d062557f63
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/metrics_visualization_test.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2018 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/value/ui/metrics_visualization.html">
+<link rel="import" href="/tracing/value/ui/visualizations_data_container.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ function generateChartBar(metrics, benchmark, page) {
+ const data = {x: benchmark, group: page};
+ for (const metric of metrics) {
+ const key = metric + '-' + benchmark;
+ const mean = Math.random() * 100;
+ data[key] = Math.round(mean * 100) / 100;
+ }
+ return data;
+ }
+
+ function generateSubmetricBar(submetric, benchmark, page,
+ metricToSubmetricMap) {
+ let submetricToBenchmarkMap = metricToSubmetricMap.get(submetric);
+ if (!submetricToBenchmarkMap) {
+ submetricToBenchmarkMap = [];
+ metricToSubmetricMap.set(submetric, submetricToBenchmarkMap);
+ }
+ const data = {x: submetric, hide: 0, group: benchmark};
+ const mean = Math.random() * 100;
+ data[submetric + '-' + benchmark] = Math.round(mean * 100) / 100;
+ submetricToBenchmarkMap.push(data);
+ }
+
+ test('instantiate', function() {
+ const mv = document.createElement('tr-v-ui-metrics-visualization');
+ this.addHTMLOutput(mv);
+
+ const testMetrics = tr.v.ui.METRICS.get('Thread');
+
+ // generate aggregate chart
+ const aggregateChart = [];
+ for (let i = 1; i <= 5; i++) {
+ aggregateChart.push(generateChartBar(testMetrics,
+ 'Run ' + i, 'aggregate'));
+ }
+
+ // generate chart with individual page metrics
+ const chartData = [];
+ for (let i = 1; i <= 5; i++) {
+ for (let j = 1; j <= 5; j++) {
+ chartData.push(generateChartBar(testMetrics,
+ 'Run ' + i, 'Page ' + j));
+ }
+ }
+
+ // generate submetrics
+ const submetricsData = new Map();
+ for (const metric in testMetrics) {
+ const testSubmetrics = [metric + 'a', metric + 'b', metric + 'c'];
+ for (let i = 1; i <= 5; i++) {
+ const page = 'Page ' + i;
+ const pageToMetricMap = tr.v.ui.getValueFromMap(page,
+ submetricsData);
+ const metricToSubmetricMap = tr.v.ui.getValueFromMap(metric,
+ pageToMetricMap);
+ for (let j = 1; j <= 5; j++) {
+ for (const submetric in testSubmetrics) {
+ generateSubmetricBar(submetric, 'Run ' + j, page,
+ metricToSubmetricMap);
+ }
+ }
+ }
+ }
+
+ mv.build({
+ title: 'Thread',
+ aggregate: aggregateChart,
+ page: chartData,
+ submetrics: submetricsData
+ });
+ });
+});
+</script> \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html
new file mode 100644
index 00000000000..b546041de50
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+
+<script>
+ 'use strict';
+ Polymer({
+ is: 'tr-v-ui-preferred-display-unit',
+
+ ready() {
+ this.preferredTimeDisplayMode_ = undefined;
+ },
+
+ attached() {
+ tr.b.Unit.didPreferredTimeDisplayUnitChange();
+ },
+
+ detached() {
+ tr.b.Unit.didPreferredTimeDisplayUnitChange();
+ },
+
+ // null means no-preference
+ get preferredTimeDisplayMode() {
+ return this.preferredTimeDisplayMode_;
+ },
+
+ set preferredTimeDisplayMode(v) {
+ if (this.preferredTimeDisplayMode_ === v) return;
+ this.preferredTimeDisplayMode_ = v;
+ tr.b.Unit.didPreferredTimeDisplayUnitChange();
+ }
+
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html
new file mode 100644
index 00000000000..382dc5963fe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/preferred_display_unit_test.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/time_display_modes.html">
+<link rel="import" href="/tracing/value/ui/preferred_display_unit.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const unit = document.createElement('tr-v-ui-preferred-display-unit');
+ const ms = tr.b.TimeDisplayModes.ms;
+ unit.preferredDisplayUnit = ms;
+ assert.strictEqual(unit.preferredDisplayUnit, ms);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html
new file mode 100644
index 00000000000..d3253d8eff9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization.html
@@ -0,0 +1,274 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2018 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<dom-module id='tr-v-ui-raster-visualization'>
+ <template>
+ <style>
+ button {
+ padding: 5px;
+ font-size: 14px;
+ }
+ .error {
+ color: red;
+ display: none;
+ }
+
+ .text_input {
+ width: 200px;
+ padding: 4px;
+ font-size: 14px;
+ }
+
+ .selector_container{
+ padding: 5px;
+ }
+
+ #search {
+ display: inline-block;
+ padding-bottom: 10px;
+ }
+
+ #search_page {
+ width: 200px;
+ }
+
+ #pageSelector {
+ display: inline-block;
+ font-size: 12pt;
+ }
+
+ #close {
+ display: none;
+ vertical-align: top;
+ }
+
+ #close svg{
+ height: 1em;
+ }
+
+ #close svg line {
+ stroke-width: 18;
+ stroke: black;
+ }
+
+ #close:hover svg {
+ background: black;
+ }
+
+ #close:hover svg line {
+ stroke: white;
+ }
+ </style>
+ <span id="aggregateContainer">
+ <div>
+ <div class="selector_container">
+ <span id="select_page_label">Individual Page Results:</span>
+ <select id="pageSelector">
+ <option id="select_page" value="">Select a page</option>
+ </select>
+ </div>
+ <div class="selector_container">
+ <div id="search_page_label">Search for a page:</div>
+ <input id="search_page" class="text_input" placeholder="Page Name">
+ <button id="search_button">Search</button>
+ <div id="search_error" class="error">Sorry, could not find that page!</div>
+ </div>
+ </div>
+ </span>
+ <span id="pageContainer">
+ <span id="close">
+ <svg viewbox="0 0 128 128">
+ <line x1="28" y1="28" x2="100" y2="100"/>
+ <line x1="28" y1="100" x2="100" y2="28"/>
+ </svg>
+ </span>
+ </span>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-v-ui-raster-visualization',
+
+ ready() {
+ this.$.pageSelector.addEventListener ('click', () => {
+ this.selectPage_();
+ });
+
+ this.$.search_page.addEventListener ('keydown', (e) => {
+ if (e.key === 'Enter') this.searchByPage_();
+ });
+
+ this.$.search_button.addEventListener ('click', () => {
+ this.searchByPage_();
+ });
+ },
+
+
+ build(chartData) {
+ this.data_ = chartData;
+ const aggregateChart = this.createChart_('Aggregate Data by Run');
+ Polymer.dom(this.$.aggregateContainer).appendChild(aggregateChart);
+ aggregateChart.enableToolTip = true;
+ aggregateChart.toolTipCallBack = (rect) =>
+ this.openBenchmarkChart_(rect);
+ this.setChartColors_(aggregateChart, this.data_.get(tr.v.ui.AGGREGATE_KEY));
+ aggregateChart.data = this.data_.get(tr.v.ui.AGGREGATE_KEY);
+ this.setChartSize_(aggregateChart,
+ this.data_.get(tr.v.ui.AGGREGATE_KEY).length);
+
+ for (const page of this.data_.keys()) {
+ if (page === tr.v.ui.AGGREGATE_KEY) continue;
+ const option = document.createElement('option');
+ option.textContent = page;
+ option.value = page;
+ this.$.pageSelector.appendChild(option);
+ }
+ },
+
+ setChartSize_(chart, pageCount, dataLength) {
+ chart.graphHeight = tr.b.math.clamp(pageCount * 25, 175, 1000);
+ chart.graphWidth = tr.b.math.clamp(pageCount * 25, 500, 1000);
+ },
+
+ setChartColors_(chart, data) {
+ const metrics = new Map();
+ let count = 0;
+ for (const thread of tr.v.ui.FRAME.values()) {
+ for (const metric of thread.keys()) {
+ metrics.set(metric, count);
+ count++;
+ }
+ }
+ for (let i = 0; i < Math.floor(data.length / tr.v.ui.FRAME.length); ++i) {
+ let j = 0;
+ for (const [threadName, thread] of tr.v.ui.FRAME.entries()) {
+ for (const metric of thread.keys()) {
+ let color = 'transparent';
+ if (thread.get(metric)) {
+ const mainColorIndex = metrics.get(metric) % tr.v.ui.COLORS.length;
+ const subColorIndex = i % tr.v.ui.COLORS[mainColorIndex].length;
+ color = tr.v.ui.COLORS[mainColorIndex][subColorIndex];
+ }
+ const series = metric + '-' + data[i * 2 + j].x + '-' + threadName;
+ chart.getDataSeries(series).color = color;
+ chart.getDataSeries(series).title = !i ? metric : '';
+ }
+ j++;
+ }
+ }
+ },
+
+ createChart_(title) {
+ const newChart = new tr.ui.b.NameBarChart();
+ newChart.chartTitle = title;
+ newChart.xAxisLabel = 'ms';
+ newChart.hideLegend = false;
+ newChart.showTitleInLegend = true;
+ newChart.hideYAxis = true;
+ newChart.isStacked = true;
+ newChart.displayXInHover = true;
+ newChart.isGrouped = true;
+ return newChart;
+ },
+
+ openBenchmarkChart_(rect) {
+ // Find main metric and corresponding sub-metrics
+ const benchmarkIndex = Math.floor(rect.index / tr.v.ui.FRAME.length);
+ const title = rect.datum.x;
+
+ // Create child chart with breakdown data
+ const div = document.createElement('div');
+ Polymer.dom(this.$.pageContainer).
+ insertBefore(div, this.$.pageContainer.firstChild);
+
+ const chart = this.createChart_(title);
+
+ div.appendChild(chart);
+ const button = this.initializeCloseButton_(div, this.$.pageContainer);
+ div.appendChild(button);
+
+ const newDataSet = [];
+
+ for (const page of this.data_.keys()) {
+ if (page === tr.v.ui.AGGREGATE_KEY) continue;
+ for (let i = 0; i < tr.v.ui.FRAME.length; i++) {
+ newDataSet.push(this.data_
+ .get(page)[benchmarkIndex * tr.v.ui.FRAME.length + i]);
+ }
+ }
+
+ this.setChartColors_(chart, newDataSet);
+ chart.data = newDataSet;
+ this.setChartSize_(chart, newDataSet.length);
+ },
+
+ selectPage_() {
+ // Create child chart with breakdown data
+ const div = document.createElement('div');
+ const page = this.$.pageSelector.value;
+ if (page === '') return;
+ Polymer.dom(this.$.pageContainer).
+ insertBefore(div, this.$.pageContainer.firstChild);
+
+ const pageChart = this.createChart_(page);
+
+ div.appendChild(pageChart);
+ const button = this.initializeCloseButton_(div, this.$.pageContainer);
+ div.appendChild(button);
+
+ const pageData = this.data_.get(page);
+
+ this.setChartColors_(pageChart, pageData);
+ pageChart.data = pageData;
+ this.setChartSize_(pageChart, pageData.length);
+ },
+
+ searchByPage_() {
+ const criteria = this.$.search_page.value;
+ if (criteria === '') return;
+
+ const query = new RegExp(criteria);
+
+ const filteredData = [...this.data_.keys()]
+ .filter(page => page.match(query));
+
+ if (filteredData.length < 1) {
+ this.$.search_error.style.display = 'block';
+ return;
+ }
+
+ // Create child chart with breakdown data
+ const page = filteredData[0];
+
+ const div = document.createElement('div');
+ Polymer.dom(this.$.pageContainer).
+ insertBefore(div, this.$.pageContainer.firstChild);
+
+ const pageChart = this.createChart_(page);
+
+ div.appendChild(pageChart);
+ const button = this.initializeCloseButton_(div, this.$.pageContainer);
+ div.appendChild(button);
+
+ const pageData = this.data_.get(page);
+
+ this.setChartColors_(pageChart, pageData);
+ pageChart.data = pageData;
+ this.setChartSize_(pageChart, pageData.length);
+ },
+
+ initializeCloseButton_(div, parent) {
+ const button = this.$.close.cloneNode(true);
+ button.style.display = 'inline-block';
+ button.addEventListener('click', () => {
+ Polymer.dom(parent).removeChild(div);
+ });
+ return button;
+ },
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html
new file mode 100644
index 00000000000..eaf3ee61842
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/raster_visualization_test.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2018 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/value/ui/raster_visualization.html">
+<link rel="import" href="/tracing/value/ui/visualizations_data_container.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function generateBars(page, benchmark) {
+ const benchmarkData = [];
+ for (const [threadName, thread] of tr.v.ui.FRAME.entries()) {
+ const data = {x: benchmark, hide: 0};
+ if (page !== tr.v.ui.AGGREGATE_KEY) data.group = page;
+ for (const metric of thread.keys()) {
+ const key = metric + '-' + data.x + '-' + threadName;
+ const mean = Math.random() * 100;
+ data[key] = Math.round(mean * 100) / 100;
+ }
+ benchmarkData.push(data);
+ }
+ return benchmarkData;
+ }
+
+ test('instantiate', function() {
+ const rv = document.createElement('tr-v-ui-raster-visualization');
+ this.addHTMLOutput(rv);
+
+ const allChartData = new Map();
+
+ // generate aggregate data
+ let aggregateData = [];
+ for (let i = 1; i <= 5; i++) {
+ aggregateData = aggregateData.concat(generateBars(tr.v.ui.AGGREGATE_KEY,
+ 'Run ' + i));
+ }
+ allChartData.set(tr.v.ui.AGGREGATE_KEY, aggregateData);
+
+ // generate data per page
+ for (let i = 1; i <= 5; i++) {
+ const page = 'Page ' + i;
+ let chartData = [];
+ for (let j = 1; j <= 5; j++) {
+ chartData = chartData.concat(generateBars(page, 'Run ' + j));
+ }
+ allChartData.set(page, chartData);
+ }
+
+ rv.build(allChartData);
+ });
+});
+</script> \ No newline at end of file
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html
new file mode 100644
index 00000000000..ac52e51aba2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html">
+
+<dom-module id="tr-v-ui-related-event-set-span">
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ Polymer({
+ is: 'tr-v-ui-related-event-set-span',
+ behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],
+
+ updateContents_() {
+ Polymer.dom(this).textContent = '';
+ const events = new tr.model.EventSet([...this.diagnostic]);
+ const link = document.createElement('tr-ui-a-analysis-link');
+ let label = events.length + ' events';
+ if (events.length === 1) {
+ const event = tr.b.getOnlyElement(events);
+ label = event.title + ' ';
+ label += tr.b.Unit.byName.timeDurationInMs.format(
+ event.duration);
+ }
+ link.setSelectionAndContent(events, label);
+ Polymer.dom(this).appendChild(link);
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html
new file mode 100644
index 00000000000..b0058a3c97b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/related_event_set_span_test.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/value/diagnostics/related_event_set.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate_RelatedEventSet0', function() {
+ const diagnostic = new tr.v.d.RelatedEventSet();
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-RELATED-EVENT-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ assert.strictEqual('0 events', span.textContent);
+ });
+
+ test('instantiate_RelatedEventSet1', function() {
+ const diagnostic = new tr.v.d.RelatedEventSet();
+ tr.c.TestUtils.newModel(function(model) {
+ const proc = model.getOrCreateProcess(1);
+ const thread = proc.getOrCreateThread(2);
+ const event = tr.c.TestUtils.newSliceEx(
+ {title: 'a', start: 0, duration: 1});
+ thread.sliceGroup.pushSlice(event);
+ diagnostic.add(event);
+ });
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-RELATED-EVENT-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ assert.strictEqual('a 1.000 ms', span.textContent);
+ });
+
+ test('instantiate_RelatedEventSet2', function() {
+ const diagnostic = new tr.v.d.RelatedEventSet();
+ tr.c.TestUtils.newModel(function(model) {
+ const proc = model.getOrCreateProcess(1);
+ const thread = proc.getOrCreateThread(2);
+ let event = tr.c.TestUtils.newSliceEx({start: 0, duration: 1});
+ thread.sliceGroup.pushSlice(event);
+ diagnostic.add(event);
+ event = tr.c.TestUtils.newSliceEx({start: 1, duration: 1});
+ thread.sliceGroup.pushSlice(event);
+ diagnostic.add(event);
+ });
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-RELATED-EVENT-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ assert.strictEqual('2 events', span.textContent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html
new file mode 100644
index 00000000000..ac71cd7a1d7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller.html
@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/raf.html">
+
+<dom-module id="tr-v-ui-scalar-context-controller">
+ <template></template>
+</dom-module>
+
+<!--
+@fileoverview Polymer element for controlling common context across scalar
+spans. To facilitate multiple separate contexts (e.g. a separate context for
+each table column), each scalar span has to specify which "context group"
+it belongs to:
+
+ +============ some container element (e.g. <div>) ============+
+ | |
+ | <tr-v-ui-scalar-context-controller> |
+ | ^ ^ |
+ | | | |
+ | v v |
+ | .... Context group 1 .... .... Context group 2 .... |
+ | : <tr-v-ui-scalar-span> : : <tr-v-ui-scalar-span> : |
+ | : <tr-v-ui-scalar-span> : : <tr-v-ui-scalar-span> : . . . |
+ | : . . . : : . . . : |
+ | :.......................: :.......................: |
+ +=============================================================+
+
+An element can find its enclosing context controller using the
+getScalarContextControllerForElement(node) defined in this file. Scalar spans
+can push their state to the controller using the following three methods:
+
+ 1. onScalarSpanAdded(contextGroup, span)
+ This method should be called when a span is attached to the DOM tree (or
+ afterwards when added to a context group).
+
+ 2. onScalarSpanRemoved(contextGroup, span)
+ This method should be called when a span is detached from the DOM tree (or
+ beforehand when removed from a context group).
+
+ 3. onScalarSpanUpdated(contextGroup, span)
+ This method should be called when the value of a span changes.
+
+Note: If a span wants to change its context group, it should first call
+onScalarSpanRemoved with the old group and then onScalarSpanAdded with the new
+group.
+
+If one or more group contexts are modified (due to one of the three methods
+above), the controller will asynchronously (at the next RAF) update them and
+fire a 'context-updated' event. Scalar spans can listen for this event and
+update their UI accordingly.
+
+The context currently consists of the range of values of the associated spans.
+This allows automatic display of relative sizes using sparklines.
+
+The controller design is based on:
+https://docs.google.com/document/d/16ih8yYK8kF8MMlPnB-5KlyfS_AjjtbyAfi3pkxoZ8xs/edit?usp=sharing
+-->
+<script>
+'use strict';
+
+tr.exportTo('tr.v.ui', function() {
+ Polymer({
+ is: 'tr-v-ui-scalar-context-controller',
+
+ created() {
+ this.host_ = undefined;
+ this.groupToContext_ = new Map();
+ this.dirtyGroups_ = new Set();
+ },
+
+ attached() {
+ if (this.host_) {
+ throw new Error(
+ 'Scalar context controller is already attached to a host');
+ }
+
+ const host = findParentOrHost(this);
+ if (host.__scalarContextController) {
+ throw new Error(
+ 'Multiple scalar context controllers attached to this host');
+ }
+
+ host.__scalarContextController = this;
+ this.host_ = host;
+ },
+
+ detached() {
+ if (!this.host_) {
+ throw new Error('Scalar context controller is not attached to a host');
+ }
+ if (this.host_.__scalarContextController !== this) {
+ throw new Error(
+ 'Scalar context controller is not attached to its host');
+ }
+
+ delete this.host_.__scalarContextController;
+ this.host_ = undefined;
+ },
+
+ getContext(group) {
+ return this.groupToContext_.get(group);
+ },
+
+ onScalarSpanAdded(group, span) {
+ let context = this.groupToContext_.get(group);
+ if (context === undefined) {
+ context = {
+ spans: new Set(),
+ range: new tr.b.math.Range()
+ };
+ this.groupToContext_.set(group, context);
+ }
+ if (context.spans.has(span)) {
+ throw new Error('Scalar span already registered with group: ' + group);
+ }
+ context.spans.add(span);
+ this.markGroupDirtyAndScheduleUpdate_(group);
+ },
+
+ onScalarSpanRemoved(group, span) {
+ const context = this.groupToContext_.get(group);
+ if (!context.spans.has(span)) {
+ throw new Error('Scalar span not registered with group: ' + group);
+ }
+ context.spans.delete(span);
+ this.markGroupDirtyAndScheduleUpdate_(group);
+ },
+
+ onScalarSpanUpdated(group, span) {
+ const context = this.groupToContext_.get(group);
+ if (!context.spans.has(span)) {
+ throw new Error('Scalar span not registered with group: ' + group);
+ }
+ this.markGroupDirtyAndScheduleUpdate_(group);
+ },
+
+ markGroupDirtyAndScheduleUpdate_(group) {
+ const alreadyDirty = this.dirtyGroups_.size > 0;
+ this.dirtyGroups_.add(group);
+ if (!alreadyDirty) {
+ tr.b.requestAnimationFrameInThisFrameIfPossible(
+ this.updateContext, this);
+ }
+ },
+
+ updateContext() {
+ const groups = this.dirtyGroups_;
+ if (groups.size === 0) return;
+ this.dirtyGroups_ = new Set();
+
+ for (const group of groups) {
+ this.updateGroup_(group);
+ }
+
+ const event = new tr.b.Event('context-updated');
+ event.groups = groups;
+ this.dispatchEvent(event);
+ },
+
+ updateGroup_(group) {
+ const context = this.groupToContext_.get(group);
+ if (context.spans.size === 0) {
+ this.groupToContext_.delete(group);
+ return;
+ }
+ context.range.reset();
+ for (const span of context.spans) {
+ context.range.addValue(span.value);
+ }
+ }
+ });
+
+ function getScalarContextControllerForElement(element) {
+ while (element) {
+ if (element.__scalarContextController) {
+ return element.__scalarContextController;
+ }
+ element = findParentOrHost(element);
+ }
+ return undefined;
+ }
+
+ function findParentOrHost(node) {
+ if (node.parentElement) {
+ return node.parentElement;
+ }
+ while (Polymer.dom(node).parentNode) {
+ node = Polymer.dom(node).parentNode;
+ }
+ return node.host;
+ }
+
+ return {
+ getScalarContextControllerForElement,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html
new file mode 100644
index 00000000000..ccb3f61d6f4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_context_controller_test.html
@@ -0,0 +1,312 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/value/ui/scalar_context_controller.html">
+
+<dom-module id="tr-v-ui-scalar-context-controller-mock-host">
+ <template>
+ <tr-v-ui-scalar-context-controller id="controller">
+ </tr-v-ui-scalar-context-controller>
+ <content></content>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const getScalarContextControllerForElement =
+ tr.v.ui.getScalarContextControllerForElement;
+
+ Polymer({
+ is: 'tr-v-ui-scalar-context-controller-mock-host'
+ });
+
+ test('getScalarContextControllerForElement', function() {
+ const root = document.createElement('div');
+ Polymer.dom(document.body).appendChild(root);
+ try {
+ assert.isUndefined(getScalarContextControllerForElement(root));
+
+ // <div> root
+ // |__<div> host1
+ // |__<tr-v-ui-scalar-context-controller> c1
+ const host1 = document.createElement('div');
+ Polymer.dom(root).appendChild(host1);
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1));
+ const c1 = document.createElement('tr-v-ui-scalar-context-controller');
+ Polymer.dom(host1).appendChild(c1);
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+
+ // <div> root
+ // |__<div> host1
+ // | |__<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // :..<tr-v-ui-scalar-context-controller> c2
+ const host2 = document.createElement(
+ 'tr-v-ui-scalar-context-controller-mock-host');
+ const c2 = host2.$.controller;
+ Polymer.dom(root).appendChild(host2);
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+ assert.strictEqual(getScalarContextControllerForElement(host2), c2);
+
+ // <div> root
+ // |__<div> host1
+ // | |__<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // | :..<tr-v-ui-scalar-context-controller> c2
+ // |__<div> divA
+ // |__<div> divB
+ const divA = document.createElement('div');
+ Polymer.dom(host2).appendChild(divA);
+ assert.strictEqual(getScalarContextControllerForElement(divA), c2);
+ const divB = document.createElement('div');
+ Polymer.dom(divA).appendChild(divB);
+ assert.strictEqual(getScalarContextControllerForElement(divB), c2);
+
+ // <div> root
+ // |__<div> host1
+ // | |_<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // | :.-<tr-v-ui-scalar-context-controller> c2
+ // |__<div> divA
+ // |__<div> divB
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host3
+ // :..<tr-v-ui-scalar-context-controller> c3
+ const host3 = document.createElement(
+ 'tr-v-ui-scalar-context-controller-mock-host');
+ Polymer.dom(divB).appendChild(host3);
+ const c3 = host3.$.controller;
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+ assert.strictEqual(getScalarContextControllerForElement(host2), c2);
+ assert.strictEqual(getScalarContextControllerForElement(divA), c2);
+ assert.strictEqual(getScalarContextControllerForElement(divB), c2);
+ assert.strictEqual(getScalarContextControllerForElement(host3), c3);
+
+ // <div> root
+ // |__<div> host1
+ // | |_<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // | :.-<tr-v-ui-scalar-context-controller> c2
+ // |__<div> divA
+ // | :.<tr-v-ui-scalar-context-controller> c4
+ // |__<div> divB
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host3
+ // :..<tr-v-ui-scalar-context-controller> c3
+ const c4 = document.createElement('tr-v-ui-scalar-context-controller');
+ Polymer.dom(divA).appendChild(c4);
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+ assert.strictEqual(getScalarContextControllerForElement(host2), c2);
+ assert.strictEqual(getScalarContextControllerForElement(divA), c4);
+ assert.strictEqual(getScalarContextControllerForElement(divB), c4);
+ assert.strictEqual(getScalarContextControllerForElement(host3), c3);
+
+ // <div> root
+ // |__<div> host1
+ // | |_<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // | :.-<tr-v-ui-scalar-context-controller> c2
+ // |__<div> divA
+ // | :.<tr-v-ui-scalar-context-controller> c4
+ // |__<div> divB
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host3
+ Polymer.dom(host3.root).removeChild(c3);
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+ assert.strictEqual(getScalarContextControllerForElement(host2), c2);
+ assert.strictEqual(getScalarContextControllerForElement(divA), c4);
+ assert.strictEqual(getScalarContextControllerForElement(divB), c4);
+ assert.strictEqual(getScalarContextControllerForElement(host3), c4);
+
+ // <div> root
+ // |__<div> host1
+ // | |_<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // |__<div> divA
+ // | :.<tr-v-ui-scalar-context-controller> c4
+ // |__<div> divB
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host3
+ Polymer.dom(host2.root).removeChild(c2);
+ assert.isUndefined(getScalarContextControllerForElement(root));
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+ assert.isUndefined(getScalarContextControllerForElement(host2));
+ assert.strictEqual(getScalarContextControllerForElement(divA), c4);
+ assert.strictEqual(getScalarContextControllerForElement(divB), c4);
+ assert.strictEqual(getScalarContextControllerForElement(host3), c4);
+
+ // <div> root
+ // | :.<tr-v-ui-scalar-context-controller> c3
+ // |__<div> host1
+ // | |_<tr-v-ui-scalar-context-controller> c1
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host2
+ // |__<div> divA
+ // | :.<tr-v-ui-scalar-context-controller> c4
+ // |__<div> divB
+ // |__<tr-v-ui-scalar-context-controller-mock-host> host3
+ Polymer.dom(root).appendChild(c3);
+ assert.strictEqual(getScalarContextControllerForElement(root), c3);
+ assert.strictEqual(getScalarContextControllerForElement(host1), c1);
+ assert.strictEqual(getScalarContextControllerForElement(host2), c3);
+ assert.strictEqual(getScalarContextControllerForElement(divA), c4);
+ assert.strictEqual(getScalarContextControllerForElement(divB), c4);
+ assert.strictEqual(getScalarContextControllerForElement(host3), c4);
+ } finally {
+ Polymer.dom(document.body).removeChild(root);
+ }
+ });
+
+ function contextTest(name, testCallback) {
+ test('context_' + name, function() {
+ const root = document.createElement('div');
+ Polymer.dom(document.body).appendChild(root);
+ try {
+ const c = document.createElement('tr-v-ui-scalar-context-controller');
+ Polymer.dom(root).appendChild(c);
+
+ let updatedGroups = []; // Fail if event fires unexpectedly.
+ c.addEventListener('context-updated', function(e) {
+ if (updatedGroups) {
+ assert.fail('Unexpected context-updated event fired.');
+ }
+ updatedGroups = Array.from(e.groups);
+ });
+
+ c.expectContextUpdatedEventForTesting =
+ function(expectedUpdatedGroups) {
+ updatedGroups = undefined;
+ tr.b.forceAllPendingTasksToRunForTest();
+ assert.sameMembers(updatedGroups, expectedUpdatedGroups);
+ };
+
+ testCallback.call(this, c);
+ } finally {
+ Polymer.dom(document.body).removeChild(root);
+ }
+ });
+ }
+
+ contextTest('singleGroup', function(c) {
+ assert.isUndefined(c.getContext('G'));
+
+ const s1 = {value: 10};
+ c.onScalarSpanAdded('G', s1);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isTrue(c.getContext('G').range.equals(
+ tr.b.math.Range.fromExplicitRange(10, 10)));
+ assert.sameMembers(Array.from(c.getContext('G').spans), [s1]);
+
+ const s2 = {value: 15};
+ c.onScalarSpanAdded('G', s2);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isTrue(c.getContext('G').range.equals(
+ tr.b.math.Range.fromExplicitRange(10, 15)));
+ assert.sameMembers(Array.from(c.getContext('G').spans), [s1, s2]);
+
+ s1.value = 5;
+ c.onScalarSpanUpdated('G', s1);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isTrue(c.getContext('G').range.equals(
+ tr.b.math.Range.fromExplicitRange(5, 15)));
+ assert.sameMembers(Array.from(c.getContext('G').spans), [s1, s2]);
+
+ c.onScalarSpanRemoved('G', s2);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isTrue(c.getContext('G').range.equals(
+ tr.b.math.Range.fromExplicitRange(5, 5)));
+ assert.sameMembers(Array.from(c.getContext('G').spans), [s1]);
+
+ const s3 = {value: 0};
+ c.onScalarSpanAdded('G', s3);
+ s2.value = 14;
+ c.onScalarSpanAdded('G', s2);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isTrue(c.getContext('G').range.equals(
+ tr.b.math.Range.fromExplicitRange(0, 14)));
+ assert.sameMembers(Array.from(c.getContext('G').spans), [s1, s2, s3]);
+
+ c.onScalarSpanRemoved('G', s1);
+ c.onScalarSpanRemoved('G', s2);
+ c.onScalarSpanRemoved('G', s3);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isUndefined(c.getContext('G'));
+
+ c.onScalarSpanAdded('G', s2);
+ c.expectContextUpdatedEventForTesting(['G']);
+ assert.isTrue(c.getContext('G').range.equals(
+ tr.b.math.Range.fromExplicitRange(14, 14)));
+ assert.sameMembers(Array.from(c.getContext('G').spans), [s2]);
+ });
+
+ contextTest('multipleGroups', function(c) {
+ assert.isUndefined(c.getContext('G1'));
+ assert.isUndefined(c.getContext('G2'));
+
+ const s1 = {value: 0};
+ c.onScalarSpanAdded('G1', s1);
+ c.expectContextUpdatedEventForTesting(['G1']);
+ assert.isTrue(c.getContext('G1').range.equals(
+ tr.b.math.Range.fromExplicitRange(0, 0)));
+ assert.sameMembers(Array.from(c.getContext('G1').spans), [s1]);
+
+ const s2 = {value: 1};
+ c.onScalarSpanAdded('G2', s2);
+ c.expectContextUpdatedEventForTesting(['G2']);
+ assert.isTrue(c.getContext('G2').range.equals(
+ tr.b.math.Range.fromExplicitRange(1, 1)));
+ assert.sameMembers(Array.from(c.getContext('G2').spans), [s2]);
+
+ const s3 = {value: 2};
+ const s4 = {value: -1};
+ c.onScalarSpanAdded('G2', s3);
+ c.onScalarSpanAdded('G1', s4);
+ c.expectContextUpdatedEventForTesting(['G1', 'G2']);
+ assert.isTrue(c.getContext('G1').range.equals(
+ tr.b.math.Range.fromExplicitRange(-1, 0)));
+ assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s4]);
+ assert.isTrue(c.getContext('G2').range.equals(
+ tr.b.math.Range.fromExplicitRange(1, 2)));
+ assert.sameMembers(Array.from(c.getContext('G2').spans), [s2, s3]);
+
+ c.onScalarSpanRemoved('G2', s3);
+ c.onScalarSpanAdded('G1', s3);
+ c.expectContextUpdatedEventForTesting(['G1', 'G2']);
+ assert.isTrue(c.getContext('G1').range.equals(
+ tr.b.math.Range.fromExplicitRange(-1, 2)));
+ assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s3, s4]);
+ assert.isTrue(c.getContext('G2').range.equals(
+ tr.b.math.Range.fromExplicitRange(1, 1)));
+ assert.sameMembers(Array.from(c.getContext('G2').spans), [s2]);
+
+ s4.value = 3;
+ c.onScalarSpanUpdated('G1', s4);
+ s1.value = 1;
+ c.onScalarSpanUpdated('G1', s1);
+ c.expectContextUpdatedEventForTesting(['G1']);
+ assert.isTrue(c.getContext('G1').range.equals(
+ tr.b.math.Range.fromExplicitRange(1, 3)));
+ assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s3, s4]);
+ assert.isTrue(c.getContext('G2').range.equals(
+ tr.b.math.Range.fromExplicitRange(1, 1)));
+ assert.sameMembers(Array.from(c.getContext('G2').spans), [s2]);
+
+ c.onScalarSpanRemoved('G2', s2);
+ c.expectContextUpdatedEventForTesting(['G2']);
+ assert.isTrue(c.getContext('G1').range.equals(
+ tr.b.math.Range.fromExplicitRange(1, 3)));
+ assert.sameMembers(Array.from(c.getContext('G1').spans), [s1, s3, s4]);
+ assert.isUndefined(c.getContext('G2'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html
new file mode 100644
index 00000000000..631c3696f8c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id="tr-v-ui-scalar-diagnostic-span">
+ <template>
+ <tr-v-ui-scalar-span id="scalar"></tr-v-ui-scalar-span>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ Polymer({
+ is: 'tr-v-ui-scalar-diagnostic-span',
+ behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],
+
+ updateContents_() {
+ this.$.scalar.setValueAndUnit(this.diagnostic.value.value,
+ this.diagnostic.value.unit);
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html
new file mode 100644
index 00000000000..9f6167f1acb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_diagnostic_span_test.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/value/diagnostics/scalar.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const diagnostic = new tr.v.d.Scalar(new tr.b.Scalar(
+ tr.b.Unit.byName.timeDurationInMs, 123.456));
+ const span = tr.v.ui.createDiagnosticSpan(diagnostic);
+ assert.strictEqual('TR-V-UI-SCALAR-DIAGNOSTIC-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html
new file mode 100644
index 00000000000..24a5cf3ac0f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id="tr-v-ui-scalar-map-table">
+ <template>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+Polymer({
+ is: 'tr-v-ui-scalar-map-table',
+
+ created() {
+ /** @type {!Map.<string, !tr.b.Scalar>} */
+ this.scalarMap_ = new Map();
+
+ /** @type {!Map.<string, !tr.b.math.Statistics.Significance>} */
+ this.significance_ = new Map();
+ },
+
+ ready() {
+ this.$.table.showHeader = false;
+ this.$.table.tableColumns = [
+ {
+ value(row) {
+ return row.name;
+ }
+ },
+ {
+ value(row) {
+ const span = tr.v.ui.createScalarSpan(row.value);
+ if (row.significance !== undefined) {
+ span.significance = row.significance;
+ } else if (row.anyRowsHaveSignificance) {
+ // Ensure vertical alignment.
+ span.style.marginRight = '18px';
+ }
+ span.style.whiteSpace = 'nowrap';
+ return span;
+ }
+ }
+ ];
+ },
+
+ get scalarMap() {
+ return this.scalarMap_;
+ },
+
+ /**
+ * @param {!Map.<string,!tr.b.Scalar>} map
+ */
+ set scalarMap(map) {
+ this.scalarMap_ = map;
+ this.updateContents_();
+ },
+
+ /**
+ * @param {string} key
+ * @param {!tr.b.math.Statistics.Significance} significance
+ */
+ setSignificanceForKey(key, significance) {
+ this.significance_.set(key, significance);
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ const rows = [];
+ for (const [key, scalar] of this.scalarMap) {
+ rows.push({
+ name: key,
+ value: scalar,
+ significance: this.significance_.get(key),
+ anyRowsHaveSignificance: (this.significance_.size > 0)
+ });
+ }
+ this.$.table.tableRows = rows;
+ this.$.table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html
new file mode 100644
index 00000000000..4c9a50c2314
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_map_table_test.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/ui/scalar_map_table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const span = document.createElement('tr-v-ui-scalar-map-table');
+
+ const histogram = new tr.v.Histogram('', tr.b.Unit.byName.energyInJoules);
+ for (let i = 0; i < 1e2; ++i) {
+ histogram.addSample(Math.random() * 1000);
+ }
+
+ histogram.addSample('foo');
+ histogram.customizeSummaryOptions({nans: true});
+
+ span.scalarMap = histogram.statisticsScalars;
+ this.addHTMLOutput(span);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html
new file mode 100644
index 00000000000..50d89653ed1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span.html
@@ -0,0 +1,626 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/ui/scalar_context_controller.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ /**
+ * One common simple way to use this function is
+ * createScalarSpan(number, {unit: tr.b.Unit.byName.whatever})
+ *
+ * This function can also take a Scalar, undefined, or a Histogram plus
+ * significance, contextGroup, customContextRange, leftAlign and/or inline.
+ *
+ * @param {undefined|tr.b.Scalar|tr.v.Histogram} value
+ * @param {Object=} opt_config
+ * @param {!tr.b.math.Range=} opt_config.customContextRange
+ * @param {boolean=} opt_config.leftAlign
+ * @param {boolean=} opt_config.inline
+ * @param {!tr.b.Unit=} opt_config.unit
+ * @param {tr.b.math.Statistics.Significance=} opt_config.significance
+ * @param {string=} opt_config.contextGroup
+ * @return {(string|!HTMLElement)}
+ */
+ function createScalarSpan(value, opt_config) {
+ if (value === undefined) return '';
+
+ const config = opt_config || {};
+ const ownerDocument = config.ownerDocument || document;
+
+ const span = ownerDocument.createElement('tr-v-ui-scalar-span');
+
+ let numericValue;
+ if (value instanceof tr.b.Scalar) {
+ span.value = value;
+ numericValue = value.value;
+ } else if (value instanceof tr.v.Histogram) {
+ numericValue = value.average;
+ if (numericValue === undefined) return '';
+ span.setValueAndUnit(numericValue, value.unit);
+ } else {
+ const unit = config.unit;
+ if (unit === undefined) {
+ throw new Error(
+ 'Unit must be provided in config when value is a number');
+ }
+ span.setValueAndUnit(value, unit);
+ numericValue = value;
+ }
+
+ if (config.context) {
+ span.context = config.context;
+ }
+
+ if (config.customContextRange) {
+ span.customContextRange = config.customContextRange;
+ }
+
+ if (config.leftAlign) {
+ span.leftAlign = true;
+ }
+
+ if (config.inline) {
+ span.inline = true;
+ }
+
+ if (config.significance !== undefined) {
+ span.significance = config.significance;
+ }
+
+ if (config.contextGroup !== undefined) {
+ span.contextGroup = config.contextGroup;
+ }
+
+ return span;
+ }
+
+ return {
+ createScalarSpan,
+ };
+});
+</script>
+
+<dom-module id="tr-v-ui-scalar-span">
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ position: relative;
+ /* Limit the sparkline's negative z-index to the span only. */
+ isolation: isolate;
+ }
+
+ :host(.left-align) {
+ justify-content: flex-start;
+ }
+
+ :host(.inline) {
+ display: inline-flex;
+ }
+
+ #sparkline {
+ width: 0%;
+ position: absolute;
+ bottom: 0;
+ display: none;
+ height: 100%;
+ background-color: hsla(216, 100%, 94.5%, .75);
+ border-color: hsl(216, 100%, 89%);
+ box-sizing: border-box;
+ z-index: -1;
+ }
+ #sparkline.positive {
+ border-right-style: solid;
+ /* The border width must be kept in sync with buildSparklineStyle_(). */
+ border-right-width: 1px;
+ }
+ #sparkline:not(.positive) {
+ border-left-style: solid;
+ /* The border width must be kept in sync with buildSparklineStyle_(). */
+ border-left-width: 1px;
+ }
+ #sparkline.better {
+ background-color: hsla(115, 100%, 93%, .75);
+ border-color: hsl(118, 60%, 80%);
+ }
+ #sparkline.worse {
+ background-color: hsla(0, 100%, 88%, .75);
+ border-color: hsl(0, 100%, 80%);
+ }
+
+ #content {
+ white-space: nowrap;
+ }
+ #content, #significance, #warning {
+ flex-grow: 0;
+ }
+ #content.better {
+ color: green;
+ }
+ #content.worse {
+ color: red;
+ }
+
+ #significance svg {
+ margin-left: 4px;
+ display: none;
+ height: 1em;
+ vertical-align: text-top;
+ stroke-width: 4;
+ fill: rgba(0, 0, 0, 0);
+ }
+ #significance #insignificant {
+ stroke: black;
+ }
+ #significance #significantly_better {
+ stroke: green;
+ }
+ #significance #significantly_worse {
+ stroke: red;
+ }
+
+ #warning {
+ display: none;
+ margin-left: 4px;
+ height: 1em;
+ vertical-align: text-top;
+ stroke-width: 0;
+ }
+ #warning path {
+ fill: rgb(255, 185, 185);
+ }
+ #warning rect {
+ fill: red;
+ }
+ </style>
+
+ <span id="sparkline"></span>
+
+ <span id="content"></span>
+
+ <span id="significance">
+ <!-- Neutral face -->
+ <svg viewbox="0 0 128 128" id="insignificant">
+ <circle r="60" cx="64" cy="64"/>
+ <circle r="4" cx="44" cy="44"/>
+ <circle r="4" cx="84" cy="44"/>
+ <line x1="36" x2="92" y1="80" y2="80"/>
+ </svg>
+
+ <!-- Smiling face -->
+ <svg viewbox="0 0 128 128" id="significantly_better">
+ <circle r="60" cx="64" cy="64"/>
+ <circle r="4" cx="44" cy="44"/>
+ <circle r="4" cx="84" cy="44"/>
+ <path d="M 28 64 Q 64 128 100 64"/>
+ </svg>
+
+ <!-- Frowning face -->
+ <svg viewbox="0 0 128 128" id="significantly_worse">
+ <circle r="60" cx="64" cy="64"/>
+ <circle r="4" cx="44" cy="44"/>
+ <circle r="4" cx="84" cy="44"/>
+ <path d="M 36 96 Q 64 48 92 96"/>
+ </svg>
+ </span>
+
+ <svg viewbox="0 0 128 128" id="warning">
+ <path d="M 64 0 L 128 128 L 0 128 L 64 0"/>
+ <rect x="60" width="8" y="0" height="84"/>
+ <rect x="60" width="8" y="100" height="24"/>
+ </svg>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-v-ui-scalar-span',
+
+ properties: {
+ /**
+ * String identifier for grouping scalar spans with common context (e.g.
+ * all scalar spans in a single table column would typically share a common
+ * context and, thus, have the same context group identifier). If falsy,
+ * the scalar span will NOT be associated with any context.
+ */
+ contextGroup: {
+ type: String,
+ reflectToAttribute: true,
+ observer: 'contextGroupChanged_'
+ }
+ },
+
+ created() {
+ this.value_ = undefined;
+ this.unit_ = undefined;
+
+ // TODO(petrcermak): Merge this into the context controller.
+ this.context_ = undefined;
+
+ this.warning_ = undefined;
+ this.significance_ = tr.b.math.Statistics.Significance.DONT_CARE;
+
+ // To avoid unnecessary DOM traversal, search for the context controller
+ // only when necessary (when the span is attached and has a context group).
+ this.shouldSearchForContextController_ = false;
+ this.lazyContextController_ = undefined;
+ this.onContextUpdated_ = this.onContextUpdated_.bind(this);
+ this.updateContents_ = this.updateContents_.bind(this);
+
+ // The span can specify a custom context range, which will override the
+ // values from the context controller.
+ this.customContextRange_ = undefined;
+ },
+
+ get significance() {
+ return this.significance_;
+ },
+
+ set significance(s) {
+ this.significance_ = s;
+ this.updateContents_();
+ },
+
+ set contentTextDecoration(deco) {
+ this.$.content.style.textDecoration = deco;
+ },
+
+ get value() {
+ return this.value_;
+ },
+
+ set value(value) {
+ if (value instanceof tr.b.Scalar) {
+ this.value_ = value.value;
+ this.unit_ = value.unit;
+ } else {
+ this.value_ = value;
+ }
+ this.updateContents_();
+ if (this.hasContext_(this.contextGroup)) {
+ this.contextController_.onScalarSpanUpdated(this.contextGroup, this);
+ } else {
+ this.updateSparkline_();
+ }
+ },
+
+ get contextController_() {
+ if (this.shouldSearchForContextController_) {
+ this.lazyContextController_ =
+ tr.v.ui.getScalarContextControllerForElement(this);
+ this.shouldSearchForContextController_ = false;
+ }
+ return this.lazyContextController_;
+ },
+
+ hasContext_(contextGroup) {
+ // The ordering here is important. It ensures that we avoid a DOM traversal
+ // when the span doesn't have a context group.
+ return !!(contextGroup && this.contextController_);
+ },
+
+ contextGroupChanged_(newContextGroup, oldContextGroup) {
+ this.detachFromContextControllerIfPossible_(oldContextGroup);
+ if (!this.attachToContextControllerIfPossible_(newContextGroup)) {
+ // If the span failed to attach to a controller, it won't receive a
+ // context-updated event, so we trigger it manually.
+ this.onContextUpdated_();
+ }
+ },
+
+ attachToContextControllerIfPossible_(contextGroup) {
+ if (!this.hasContext_(contextGroup)) return false;
+
+ this.contextController_.addEventListener(
+ 'context-updated', this.onContextUpdated_);
+ this.contextController_.onScalarSpanAdded(contextGroup, this);
+ return true;
+ },
+
+ detachFromContextControllerIfPossible_(contextGroup) {
+ if (!this.hasContext_(contextGroup)) return;
+
+ this.contextController_.removeEventListener(
+ 'context-updated', this.onContextUpdated_);
+ this.contextController_.onScalarSpanRemoved(contextGroup, this);
+ },
+
+ attached() {
+ tr.b.Unit.addEventListener(
+ 'display-mode-changed', this.updateContents_);
+ this.shouldSearchForContextController_ = true;
+ this.attachToContextControllerIfPossible_(this.contextGroup);
+ },
+
+ detached() {
+ tr.b.Unit.removeEventListener(
+ 'display-mode-changed', this.updateContents_);
+ this.detachFromContextControllerIfPossible_(this.contextGroup);
+ this.shouldSearchForContextController_ = false;
+ this.lazyContextController_ = undefined;
+ },
+
+ onContextUpdated_() {
+ this.updateSparkline_();
+ },
+
+ get context() {
+ return this.context_;
+ },
+
+ set context(context) {
+ this.context_ = context;
+ this.updateContents_();
+ },
+
+ get unit() {
+ return this.unit_;
+ },
+
+ set unit(unit) {
+ this.unit_ = unit;
+ this.updateContents_();
+ this.updateSparkline_();
+ },
+
+ setValueAndUnit(value, unit) {
+ this.value_ = value;
+ this.unit_ = unit;
+ this.updateContents_();
+ },
+
+ get customContextRange() {
+ return this.customContextRange_;
+ },
+
+ set customContextRange(customContextRange) {
+ this.customContextRange_ = customContextRange;
+ this.updateSparkline_();
+ },
+
+ get inline() {
+ return Polymer.dom(this).classList.contains('inline');
+ },
+
+ set inline(inline) {
+ if (inline) {
+ Polymer.dom(this).classList.add('inline');
+ } else {
+ Polymer.dom(this).classList.remove('inline');
+ }
+ },
+
+ get leftAlign() {
+ return Polymer.dom(this).classList.contains('left-align');
+ },
+
+ set leftAlign(leftAlign) {
+ if (leftAlign) {
+ Polymer.dom(this).classList.add('left-align');
+ } else {
+ Polymer.dom(this).classList.remove('left-align');
+ }
+ },
+
+ updateSparkline_() {
+ Polymer.dom(this.$.sparkline).classList.remove('positive');
+ Polymer.dom(this.$.sparkline).classList.remove('better');
+ Polymer.dom(this.$.sparkline).classList.remove('worse');
+ Polymer.dom(this.$.sparkline).classList.remove('same');
+ this.$.sparkline.style.display = 'none';
+ this.$.sparkline.style.left = '0';
+ this.$.sparkline.style.width = '0';
+
+ // Custom context range takes precedence over controller context range.
+ let range = this.customContextRange_;
+ if (!range && this.hasContext_(this.contextGroup)) {
+ const context = this.contextController_.getContext(this.contextGroup);
+ if (context) {
+ range = context.range;
+ }
+ }
+ if (!range || range.isEmpty) return;
+
+ const leftPoint = Math.min(range.min, 0);
+ const rightPoint = Math.max(range.max, 0);
+ const pointDistance = rightPoint - leftPoint;
+ if (pointDistance === 0) {
+ // This can happen, for example, when all spans within the context have
+ // zero values (so |range| is [0, 0]).
+ return;
+ }
+
+ // Draw the sparkline.
+ this.$.sparkline.style.display = 'block';
+ let left;
+ let width;
+ if (this.value > 0) {
+ width = Math.min(this.value, rightPoint);
+ left = -leftPoint;
+ Polymer.dom(this.$.sparkline).classList.add('positive');
+ } else if (this.value <= 0) {
+ width = -Math.max(this.value, leftPoint);
+ left = (-leftPoint) - width;
+ }
+ this.$.sparkline.style.left = this.buildSparklineStyle_(
+ left / pointDistance, false);
+ this.$.sparkline.style.width = this.buildSparklineStyle_(
+ width / pointDistance, true);
+
+ // Set the sparkline color (if applicable).
+ const changeClass = this.changeClassName_;
+ if (changeClass) {
+ Polymer.dom(this.$.sparkline).classList.add(changeClass);
+ }
+ },
+
+ buildSparklineStyle_(ratio, isWidth) {
+ // To avoid visual glitches around the zero value bar, we subtract 1 pixel
+ // from the width of the element and multiply the remainder (100% - 1px) by
+ // |ratio|. The extra pixel is used for the sparkline border. This allows
+ // us to align zero sparklines with both positive and negative values:
+ //
+ // ::::::::::| +10 MiB
+ // :::::| +5 MiB
+ // | 0 MiB
+ // |::::: -5 MiB
+ // |:::::::::: -10 MiB
+ //
+ let position = 'calc(' + ratio + ' * (100% - 1px)';
+ if (isWidth) {
+ position += ' + 1px'; // Extra pixel for sparkline border.
+ }
+ position += ')';
+ return position;
+ },
+
+ updateContents_() {
+ Polymer.dom(this.$.content).textContent = '';
+ Polymer.dom(this.$.content).classList.remove('better');
+ Polymer.dom(this.$.content).classList.remove('worse');
+ Polymer.dom(this.$.content).classList.remove('same');
+ this.$.insignificant.style.display = '';
+ this.$.significantly_better.style.display = '';
+ this.$.significantly_worse.style.display = '';
+
+ if (this.unit_ === undefined) return;
+
+ this.$.content.title = '';
+ Polymer.dom(this.$.content).textContent =
+ this.unit_.format(this.value, this.context);
+ this.updateDelta_();
+ },
+
+ updateDelta_() {
+ let changeClass = this.changeClassName_;
+ if (!changeClass) {
+ this.$.significance.style.display = 'none';
+ return; // Not a delta or we don't care.
+ }
+
+ this.$.significance.style.display = 'inline';
+
+ let title;
+ switch (changeClass) {
+ case 'better':
+ title = 'improvement';
+ break;
+
+ case 'worse':
+ title = 'regression';
+ break;
+
+ case 'same':
+ title = 'no change';
+ break;
+
+ default:
+ throw new Error('Unknown change class: ' + changeClass);
+ }
+
+ // Set the content class separately from the significance class so that
+ // the Neutral face is always a neutral color.
+ Polymer.dom(this.$.content).classList.add(changeClass);
+
+ switch (this.significance) {
+ case tr.b.math.Statistics.Significance.DONT_CARE:
+ break;
+
+ case tr.b.math.Statistics.Significance.INSIGNIFICANT:
+ if (changeClass !== 'same') title = 'insignificant ' + title;
+ this.$.insignificant.style.display = 'inline';
+ changeClass = 'same';
+ break;
+
+ case tr.b.math.Statistics.Significance.SIGNIFICANT:
+ if (changeClass === 'same') {
+ throw new Error('How can no change be significant?');
+ }
+ this.$['significantly_' + changeClass].style.display = 'inline';
+ title = 'significant ' + title;
+ break;
+
+ default:
+ throw new Error('Unknown significance ' + this.significance);
+ }
+
+ this.$.significance.title = title;
+ this.$.content.title = title;
+ },
+
+ get changeClassName_() {
+ if (!this.unit_ || !this.unit_.isDelta) return undefined;
+
+ switch (this.unit_.improvementDirection) {
+ case tr.b.ImprovementDirection.DONT_CARE:
+ return undefined;
+
+ case tr.b.ImprovementDirection.BIGGER_IS_BETTER:
+ if (this.value === 0) return 'same';
+ return this.value > 0 ? 'better' : 'worse';
+
+ case tr.b.ImprovementDirection.SMALLER_IS_BETTER:
+ if (this.value === 0) return 'same';
+ return this.value < 0 ? 'better' : 'worse';
+
+ default:
+ throw new Error('Unknown improvement direction: ' +
+ this.unit_.improvementDirection);
+ }
+ },
+
+ get warning() {
+ return this.warning_;
+ },
+
+ set warning(warning) {
+ this.warning_ = warning;
+ const warningEl = this.$.warning;
+ if (this.warning_) {
+ warningEl.title = warning;
+ warningEl.style.display = 'inline';
+ } else {
+ warningEl.title = '';
+ warningEl.style.display = '';
+ }
+ },
+
+ // tr-v-ui-time-stamp-span property
+ get timestamp() {
+ return this.value;
+ },
+
+ set timestamp(timestamp) {
+ if (timestamp instanceof tr.b.u.TimeStamp) {
+ this.value = timestamp;
+ return;
+ }
+ this.setValueAndUnit(timestamp, tr.b.u.Units.timeStampInMs);
+ },
+
+ // tr-v-ui-time-duration-span property
+ get duration() {
+ return this.value;
+ },
+
+ set duration(duration) {
+ if (duration instanceof tr.b.u.TimeDuration) {
+ this.value = duration;
+ return;
+ }
+ this.setValueAndUnit(duration, tr.b.u.Units.timeDurationInMs);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html
new file mode 100644
index 00000000000..56ca6917d71
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/scalar_span_test.html
@@ -0,0 +1,1027 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/time_display_modes.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/ui/scalar_context_controller.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Scalar = tr.b.Scalar;
+ const Unit = tr.b.Unit;
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ const EXAMPLE_MEMORY_FORMATTING_CONTEXT = {
+ unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI,
+ };
+ const EXAMPLE_MEMORY_NUMERIC = new Scalar(
+ Unit.byName.sizeInBytesDelta_smallerIsBetter, 256 * 1024 * 1024);
+
+ function checkSignificance(span, expectedSignificance) {
+ assert.strictEqual(span.$.insignificant.style.display,
+ expectedSignificance === 'insignificant' ? 'inline' : '');
+ assert.strictEqual(span.$.significantly_better.style.display,
+ expectedSignificance === 'significantly_better' ? 'inline' : '');
+ assert.strictEqual(span.$.significantly_worse.style.display,
+ expectedSignificance === 'significantly_worse' ? 'inline' : '');
+ }
+
+ function checkScalarSpan(test, value, unit, expectedContent, opt_options) {
+ const options = opt_options || {};
+ const span = tr.v.ui.createScalarSpan(new tr.b.Scalar(unit, value),
+ {significance: options.significance});
+
+ test.addHTMLOutput(span);
+ assert.strictEqual(
+ Polymer.dom(span.$.content).textContent, expectedContent);
+ assert.strictEqual(window.getComputedStyle(span.$.content).color,
+ options.expectedColor || 'rgb(0, 0, 0)');
+
+ if (options.expectedTitle) {
+ assert.strictEqual(span.$.content.title, options.expectedTitle);
+ }
+
+ if (options.significance !== undefined) {
+ checkSignificance(span, options.expectedEmoji);
+ if (options.expectedTitle) {
+ assert.strictEqual(span.$.significance.title, options.expectedTitle);
+ }
+ }
+ }
+
+ test('instantiate_significance', function() {
+ const countD = Unit.byName.count.correspondingDeltaUnit;
+ const countSIBD = Unit.byName.count_smallerIsBetter.correspondingDeltaUnit;
+ const countBIBD = Unit.byName.count_biggerIsBetter.correspondingDeltaUnit;
+
+ const zero = String.fromCharCode(177) + '0';
+
+ checkScalarSpan(this, 0, countSIBD, zero, {
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedTitle: 'no change',
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, 0, countSIBD, zero, {
+ expectedEmoji: 'insignificant',
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedEmojiColor: 'rgb(0, 0, 0)',
+ expectedTitle: 'no change'
+ });
+
+ assert.throws(() => checkScalarSpan(this, 0, countSIBD, zero,
+ {significance: tr.b.math.Statistics.Significance.SIGNIFICANT}));
+
+ checkScalarSpan(this, 0, countBIBD, zero, {
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedTitle: 'no change',
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, 0, countBIBD, zero, {
+ expectedEmoji: 'insignificant',
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedEmojiColor: 'rgb(0, 0, 0)',
+ expectedTitle: 'no change'
+ });
+
+ assert.throws(() => checkScalarSpan(this, 0, countSIBD, zero,
+ {significance: tr.b.math.Statistics.Significance.SIGNIFICANT}));
+
+ checkScalarSpan(this, 1, countSIBD, '+1', {
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedColor: 'rgb(255, 0, 0)',
+ expectedTitle: 'regression',
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, 1, countSIBD, '+1', {
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedColor: 'rgb(255, 0, 0)',
+ expectedEmoji: 'insignificant',
+ expectedEmojiColor: 'rgb(0, 0, 0)',
+ expectedTitle: 'insignificant regression'
+ });
+
+ checkScalarSpan(this, 1, countSIBD, '+1', {
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ expectedColor: 'rgb(255, 0, 0)',
+ expectedEmoji: 'significantly_worse',
+ expectedEmojiColor: 'rgb(255, 0, 0)',
+ expectedTitle: 'significant regression'
+ });
+
+ checkScalarSpan(this, 1, countBIBD, '+1', {
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedColor: 'rgb(0, 128, 0)',
+ expectedTitle: 'improvement',
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, 1, countBIBD, '+1', {
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedColor: 'rgb(0, 128, 0)',
+ expectedEmoji: 'insignificant',
+ expectedEmojiColor: 'rgb(0, 0, 0)',
+ expectedTitle: 'insignificant improvement'
+ });
+
+ checkScalarSpan(this, 1, countBIBD, '+1', {
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ expectedColor: 'rgb(0, 128, 0)',
+ expectedEmoji: 'significantly_better',
+ expectedEmojiColor: 'rgb(0, 128, 0)',
+ expectedTitle: 'significant improvement'
+ });
+
+ checkScalarSpan(this, -1, countSIBD, '-1', {
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedColor: 'rgb(0, 128, 0)',
+ expectedEmoji: '',
+ expectedEmojiColor: '',
+ expectedTitle: 'improvement'
+ });
+
+ checkScalarSpan(this, -1, countSIBD, '-1', {
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedColor: 'rgb(0, 128, 0)',
+ expectedEmoji: 'insignificant',
+ expectedEmojiColor: 'rgb(0, 0, 0)',
+ expectedTitle: 'insignificant improvement'
+ });
+
+ checkScalarSpan(this, -1, countSIBD, '-1', {
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ expectedColor: 'rgb(0, 128, 0)',
+ expectedEmoji: 'significantly_better',
+ expectedEmojiColor: 'rgb(0, 128, 0)',
+ expectedTitle: 'significant improvement'
+ });
+
+ checkScalarSpan(this, -1, countBIBD, '-1', {
+ expectedColor: 'rgb(255, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, -1, countBIBD, '-1', {
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedColor: 'rgb(255, 0, 0)',
+ expectedEmoji: 'insignificant',
+ expectedEmojiColor: 'rgb(0, 0, 0)',
+ expectedTitle: 'insignificant regression'
+ });
+
+ checkScalarSpan(this, -1, countBIBD, '-1', {
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ expectedColor: 'rgb(255, 0, 0)',
+ expectedEmoji: 'significantly_worse',
+ expectedEmojiColor: 'rgb(255, 0, 0)',
+ expectedTitle: 'significant regression'
+ });
+
+ checkScalarSpan(this, 1, countD, '+1', {
+ expectedColor: 'rgb(0, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, 1, countD, '+1', {
+ expectedColor: 'rgb(0, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, 1, countD, '+1', {
+ expectedColor: 'rgb(0, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, -1, countD, '-1', {
+ expectedColor: 'rgb(0, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.DONT_CARE,
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, -1, countD, '-1', {
+ expectedColor: 'rgb(0, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.INSIGNIFICANT,
+ expectedEmoji: ''
+ });
+
+ checkScalarSpan(this, -1, countD, '-1', {
+ expectedColor: 'rgb(0, 0, 0)',
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ expectedEmoji: ''
+ });
+ });
+
+ test('instantiate', function() {
+ checkScalarSpan(this, 123.456789, Unit.byName.timeDurationInMs,
+ '123.457 ms');
+ checkScalarSpan(this, 0, Unit.byName.normalizedPercentage, '0.0%');
+ checkScalarSpan(this, 1, Unit.byName.normalizedPercentage, '100.0%');
+ checkScalarSpan(this, -2560, Unit.byName.sizeInBytes, '-2.5 KiB');
+ });
+
+ test('instantiate_smallerIsBetter', function() {
+ checkScalarSpan(this, 45097156608, Unit.byName.sizeInBytes_smallerIsBetter,
+ '42.0 GiB');
+ checkScalarSpan(this, 0, Unit.byName.energyInJoules_smallerIsBetter,
+ '0.000 J');
+ checkScalarSpan(this, -0.25, Unit.byName.unitlessNumber_smallerIsBetter,
+ '-0.250');
+ });
+
+ test('instantiate_biggerIsBetter', function() {
+ checkScalarSpan(this, 0.07, Unit.byName.powerInWatts_smallerIsBetter,
+ '70.000 mW');
+ checkScalarSpan(this, 0, Unit.byName.timeStampInMs_biggerIsBetter,
+ '0.000 ms');
+ checkScalarSpan(this, -0.003,
+ Unit.byName.normalizedPercentage_biggerIsBetter, '-0.3%');
+ });
+
+ test('instantiate_delta', function() {
+ checkScalarSpan(this, 123.456789, Unit.byName.timeDurationInMsDelta,
+ '+123.457 ms');
+ checkScalarSpan(this, 0, Unit.byName.normalizedPercentageDelta,
+ '\u00B10.0%');
+ checkScalarSpan(this, -2560, Unit.byName.sizeInBytesDelta,
+ '-2.5 KiB');
+ });
+
+ test('instantiate_delta_smallerIsBetter', function() {
+ checkScalarSpan(this, 45097156608,
+ Unit.byName.sizeInBytesDelta_smallerIsBetter, '+42.0 GiB',
+ {expectedColor: 'rgb(255, 0, 0)'});
+ checkScalarSpan(this, 0, Unit.byName.energyInJoulesDelta_smallerIsBetter,
+ '\u00B10.000 J');
+ checkScalarSpan(this, -0.25,
+ Unit.byName.unitlessNumberDelta_smallerIsBetter, '-0.250',
+ {expectedColor: 'rgb(0, 128, 0)'});
+ });
+
+ test('instantiate_delta_biggerIsBetter', function() {
+ checkScalarSpan(this, 0.07, Unit.byName.powerInWattsDelta_biggerIsBetter,
+ '+70.000 mW', {expectedColor: 'rgb(0, 128, 0)'});
+ checkScalarSpan(this, 0, Unit.byName.timeStampInMsDelta_biggerIsBetter,
+ '\u00B10.000 ms');
+ checkScalarSpan(this, -0.003,
+ Unit.byName.normalizedPercentageDelta_biggerIsBetter, '-0.3%',
+ {expectedColor: 'rgb(255, 0, 0)'});
+ });
+
+ test('createScalarSpan', function() {
+ // No config.
+ let span = tr.v.ui.createScalarSpan(
+ new Scalar(Unit.byName.powerInWatts, 3.14));
+ assert.strictEqual(Polymer.dom(span.$.content).textContent, '3.140 W');
+ assert.strictEqual(span.ownerDocument, document);
+ assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(span.value, 3.14);
+ assert.strictEqual(span.unit, Unit.byName.powerInWatts);
+ assert.isUndefined(span.context);
+ assert.isUndefined(span.customContextRange);
+ assert.isUndefined(span.warning);
+ assert.isFalse(span.leftAlign);
+ this.addHTMLOutput(span);
+
+ // Inline.
+ const div = document.createElement('div');
+ this.addHTMLOutput(div);
+ const inlineSpan = tr.v.ui.createScalarSpan(
+ new Scalar(Unit.byName.powerInWatts, 3.14),
+ {inline: true});
+ assert.strictEqual(Polymer.dom(inlineSpan.$.content).textContent,
+ '3.140 W');
+ assert.strictEqual(inlineSpan.ownerDocument, document);
+ assert.strictEqual(inlineSpan.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(inlineSpan.value, 3.14);
+ assert.strictEqual(inlineSpan.unit, Unit.byName.powerInWatts);
+ assert.isUndefined(inlineSpan.context);
+ assert.isUndefined(inlineSpan.customContextRange);
+ assert.isUndefined(inlineSpan.warning);
+ assert.isFalse(inlineSpan.leftAlign);
+ div.appendChild(document.createTextNode('prefix '));
+ div.appendChild(inlineSpan);
+ div.appendChild(document.createTextNode(' suffix'));
+ assert.isBelow(inlineSpan.getBoundingClientRect().width,
+ span.getBoundingClientRect().width);
+
+ // Custom owner document and right align.
+ span = tr.v.ui.createScalarSpan(
+ new Scalar(Unit.byName.energyInJoules, 2.72),
+ { ownerDocument: THIS_DOC, leftAlign: true});
+ assert.strictEqual(Polymer.dom(span.$.content).textContent, '2.720 J');
+ assert.strictEqual(span.ownerDocument, THIS_DOC);
+ assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(span.value, 2.72);
+ assert.strictEqual(span.unit, Unit.byName.energyInJoules);
+ assert.isUndefined(span.context);
+ assert.isUndefined(span.customContextRange);
+ assert.isUndefined(span.warning);
+ assert.isTrue(span.leftAlign);
+ this.addHTMLOutput(span);
+
+ // Unit and sparkline set via config.
+ span = tr.v.ui.createScalarSpan(1.62, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: tr.b.math.Range.fromExplicitRange(0, 3.24)
+ });
+ assert.strictEqual(Polymer.dom(span.$.content).textContent, '1.620 ms');
+ assert.strictEqual(span.ownerDocument, document);
+ assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(span.value, 1.62);
+ assert.strictEqual(span.unit, Unit.byName.timeStampInMs);
+ assert.isUndefined(span.context);
+ assert.isTrue(tr.b.math.Range.fromExplicitRange(0, 3.24).equals(
+ span.customContextRange));
+ assert.isUndefined(span.warning);
+ assert.isFalse(span.leftAlign);
+ this.addHTMLOutput(span);
+
+ // Custom context.
+ span = tr.v.ui.createScalarSpan(
+ new Scalar(Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ 256 * 1024 * 1024), { context: {
+ unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI,
+ minimumFractionDigits: 2
+ } });
+ assert.strictEqual(
+ Polymer.dom(span.$.content).textContent, '+262,144.00 KiB');
+ assert.strictEqual(span.ownerDocument, document);
+ assert.strictEqual(span.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(span.value, 256 * 1024 * 1024);
+ assert.strictEqual(span.unit, Unit.byName.sizeInBytesDelta_smallerIsBetter);
+ assert.deepEqual(span.context, {
+ unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI,
+ minimumFractionDigits: 2
+ });
+ assert.isUndefined(span.customContextRange);
+ assert.isUndefined(span.warning);
+ assert.isFalse(span.leftAlign);
+ this.addHTMLOutput(span);
+ });
+
+ test('instantiate_withWarning', function() {
+ const span = document.createElement('tr-v-ui-scalar-span');
+ span.value = 400000000;
+ span.unit = Unit.byName.sizeInBytes;
+ span.warning = 'There is a problem with this size';
+ this.addHTMLOutput(span);
+ });
+
+ test('instantiate_withCustomContextRange', function() {
+ const span = document.createElement('tr-v-ui-scalar-span');
+ span.value = new Scalar(Unit.byName.unitlessNumber, 0.99);
+ span.customContextRange = tr.b.math.Range.fromExplicitRange(0, 3);
+ this.addHTMLOutput(span);
+ });
+
+ test('instantiate_withRightAlign', function() {
+ const span = document.createElement('tr-v-ui-scalar-span');
+ span.value = new Scalar(Unit.byName.timeStampInMs, 5.777);
+ span.leftAlign = true;
+ this.addHTMLOutput(span);
+ });
+
+ test('instantiate_withContext', function() {
+ const span = document.createElement('tr-v-ui-scalar-span');
+ span.value = new Scalar(
+ Unit.byName.unitlessNumberDelta_smallerIsBetter, 42);
+ span.context = { maximumFractionDigits: 2 };
+ assert.strictEqual(Polymer.dom(span.$.content).textContent, '+42.00');
+ this.addHTMLOutput(span);
+ });
+
+ test('deltaAndNonDeltaHaveSimilarHeights', function() {
+ const spanA = document.createElement('tr-v-ui-scalar-span');
+ spanA.setValueAndUnit(400, Unit.byName.timeDurationInMs);
+ checkSignificance(spanA, '');
+
+ const spanB = document.createElement('tr-v-ui-scalar-span');
+ spanB.setValueAndUnit(400, Unit.byName.timeDurationInMsDelta);
+ checkSignificance(spanB, '');
+
+ const spanC = document.createElement('tr-v-ui-scalar-span');
+ spanC.setValueAndUnit(
+ 400, Unit.byName.timeDurationInMsDelta_smallerIsBetter);
+ spanC.significance = tr.b.math.Statistics.Significance.SIGNIFICANT;
+ checkSignificance(spanC, 'significantly_worse');
+
+ const spanD = document.createElement('tr-v-ui-scalar-span');
+ spanD.setValueAndUnit(
+ 400, Unit.byName.timeDurationInMsDelta_biggerIsBetter);
+ spanD.significance = tr.b.math.Statistics.Significance.SIGNIFICANT;
+ checkSignificance(spanD, 'significantly_better');
+
+ const spanE = document.createElement('tr-v-ui-scalar-span');
+ spanE.setValueAndUnit(
+ 400, Unit.byName.timeDurationInMsDelta_smallerIsBetter);
+ spanE.significance = tr.b.math.Statistics.Significance.INSIGNIFICANT;
+ checkSignificance(spanE, 'insignificant');
+
+ const overall = document.createElement('div');
+ overall.style.display = 'flex';
+ // These spans must be on separate lines so that Chrome has the option of
+ // making their heights different. The point of the test is that Chrome
+ // shouldn't have to make their heights different even when it could.
+ overall.style.flexDirection = 'column';
+ Polymer.dom(overall).appendChild(spanA);
+ Polymer.dom(overall).appendChild(spanB);
+ Polymer.dom(overall).appendChild(spanC);
+ Polymer.dom(overall).appendChild(spanD);
+ Polymer.dom(overall).appendChild(spanE);
+ this.addHTMLOutput(overall);
+
+ const expectedHeight = spanA.getBoundingClientRect().height;
+ assert.strictEqual(expectedHeight, spanB.getBoundingClientRect().height);
+ assert.strictEqual(expectedHeight, spanC.getBoundingClientRect().height);
+ assert.strictEqual(expectedHeight, spanD.getBoundingClientRect().height);
+ assert.strictEqual(expectedHeight, spanE.getBoundingClientRect().height);
+ });
+
+ test('warningAndNonWarningHaveSimilarHeights', function() {
+ const spanA = document.createElement('tr-v-ui-scalar-span');
+ spanA.setValueAndUnit(400, Unit.byName.timeDurationInMs);
+
+ const spanB = document.createElement('tr-v-ui-scalar-span');
+ spanB.setValueAndUnit(400, Unit.byName.timeDurationInMs);
+ spanB.warning = 'There is a problem with this time';
+
+ const overall = document.createElement('div');
+ overall.style.display = 'flex';
+ // These spans must be on separate lines so that Chrome has the option of
+ // making their heights different. The point of the test is that Chrome
+ // shouldn't have to make their heights different even when it could.
+ overall.style.flexDirection = 'column';
+ Polymer.dom(overall).appendChild(spanA);
+ Polymer.dom(overall).appendChild(spanB);
+ this.addHTMLOutput(overall);
+
+ const rectA = spanA.getBoundingClientRect();
+ const rectB = spanB.getBoundingClientRect();
+ assert.strictEqual(rectA.height, rectB.height);
+ });
+
+ test('respectCurrentDisplayUnit', function() {
+ try {
+ Unit.currentTimeDisplayMode = tr.b.TimeDisplayModes.ns;
+
+ const span = document.createElement('tr-v-ui-scalar-span');
+ span.setValueAndUnit(73, Unit.byName.timeStampInMs);
+ this.addHTMLOutput(span);
+
+ assert.isTrue(Polymer.dom(span.$.content).textContent.indexOf('ns') > 0);
+ Unit.currentTimeDisplayMode = tr.b.TimeDisplayModes.ms;
+ assert.isTrue(Polymer.dom(span.$.content).textContent.indexOf('ms') > 0);
+ } finally {
+ Unit.reset();
+ }
+ });
+
+ function checkSparkline(span, expectation) {
+ tr.b.forceAllPendingTasksToRunForTest();
+ const sparklineEl = span.$.sparkline;
+ const computedStyle = getComputedStyle(sparklineEl);
+
+ const expectedDisplay = expectation.display || 'block';
+ assert.strictEqual(computedStyle.display, expectedDisplay);
+ if (expectedDisplay === 'none') {
+ // Test expectation sanity check.
+ assert.notProperty(expectation, 'left');
+ assert.notProperty(expectation, 'width');
+ assert.notProperty(expectation, 'classList');
+ return;
+ }
+
+ assert.closeTo(parseFloat(computedStyle.left), expectation.left, 0.1);
+ assert.closeTo(parseFloat(computedStyle.width), expectation.width, 0.1);
+ assert.sameMembers(Array.from(sparklineEl.classList),
+ expectation.classList || []);
+ }
+
+ test('customContextRange', function() {
+ const div = document.createElement('div');
+ div.style.width = '101px'; // One extra pixel for sparkline border.
+ this.addHTMLOutput(div);
+
+ // No custom context range.
+ const span1 = tr.v.ui.createScalarSpan(0, {
+ unit: Unit.byName.timeStampInMs
+ });
+ Polymer.dom(div).appendChild(span1);
+ checkSparkline(span1, {display: 'none'});
+ const span2 = tr.v.ui.createScalarSpan(0, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: undefined
+ });
+ Polymer.dom(div).appendChild(span2);
+ checkSparkline(span2, {display: 'none'});
+ const span3 = tr.v.ui.createScalarSpan(0, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: new tr.b.math.Range() // Empty range.
+ });
+ Polymer.dom(div).appendChild(span3);
+ checkSparkline(span3, {display: 'none'});
+
+ const range = tr.b.math.Range.fromExplicitRange(-15, 15);
+
+ // Values inside custom context range.
+ const span4 = tr.v.ui.createScalarSpan(-15, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span4);
+ checkSparkline(span4, {left: 0, width: 51});
+ const span5 = tr.v.ui.createScalarSpan(-14, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span5);
+ checkSparkline(span5, {left: 3.33, width: 47.67});
+ const span6 = tr.v.ui.createScalarSpan(-10, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span6);
+ checkSparkline(span6, {left: 16.67, width: 34.33});
+ const span7 = tr.v.ui.createScalarSpan(0, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span7);
+ checkSparkline(span7, {left: 50, width: 1});
+ const span8 = tr.v.ui.createScalarSpan(10, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span8);
+ checkSparkline(span8, {left: 50, width: 34.33, classList: ['positive']});
+ const span9 = tr.v.ui.createScalarSpan(14, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span9);
+ checkSparkline(span9, {left: 50, width: 47.67, classList: ['positive']});
+ const span10 = tr.v.ui.createScalarSpan(15, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span10);
+ checkSparkline(span10, {left: 50, width: 51, classList: ['positive']});
+
+ // Values outside custom context range.
+ const span11 = tr.v.ui.createScalarSpan(-20, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span11);
+ checkSparkline(span11, {left: 0, width: 51});
+ const span12 = tr.v.ui.createScalarSpan(20, {
+ unit: Unit.byName.timeStampInMs,
+ customContextRange: range
+ });
+ Polymer.dom(div).appendChild(span12);
+ checkSparkline(span12, {left: 50, width: 51, classList: ['positive']});
+ });
+
+ test('emptyNumeric', function() {
+ assert.strictEqual(tr.v.ui.createScalarSpan(), '');
+ });
+
+ test('contextControllerChanges', function() {
+ const div = document.createElement('div');
+ div.style.width = '101px'; // One extra pixel for sparkline border.
+ this.addHTMLOutput(div);
+
+ div.appendChild(
+ document.createElement('tr-v-ui-scalar-context-controller'));
+
+ const s1 = tr.v.ui.createScalarSpan(10, {
+ unit: Unit.byName.powerInWatts
+ });
+ Polymer.dom(div).appendChild(s1);
+ checkSparkline(s1, {display: 'none'});
+
+ const s2 = tr.v.ui.createScalarSpan(20, {
+ unit: Unit.byName.powerInWatts,
+ contextGroup: 'A'
+ });
+ Polymer.dom(div).appendChild(s2);
+ checkSparkline(s1, {display: 'none'});
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+
+ const s3 = tr.v.ui.createScalarSpan(30, {
+ unit: Unit.byName.powerInWatts,
+ contextGroup: 'A'
+ });
+ Polymer.dom(div).appendChild(s3);
+ checkSparkline(s1, {display: 'none'});
+ checkSparkline(s2, {left: 0, width: 67.67, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+
+ const s4 = tr.v.ui.createScalarSpan(40, {
+ unit: Unit.byName.powerInWatts,
+ contextGroup: 'B'
+ });
+ Polymer.dom(div).appendChild(s4);
+ checkSparkline(s1, {display: 'none'});
+ checkSparkline(s2, {left: 0, width: 67.67, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {left: 0, width: 101, classList: ['positive']});
+
+ s3.contextGroup = 'B';
+ checkSparkline(s1, {display: 'none'});
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 76, classList: ['positive']});
+ checkSparkline(s4, {left: 0, width: 101, classList: ['positive']});
+
+ s1.setAttribute('context-group', 'A');
+ checkSparkline(s1, {left: 0, width: 51, classList: ['positive']});
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 76, classList: ['positive']});
+ checkSparkline(s4, {left: 0, width: 101, classList: ['positive']});
+
+ s1.value = 50;
+ checkSparkline(s1, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s2, {left: 0, width: 41, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 76, classList: ['positive']});
+ checkSparkline(s4, {left: 0, width: 101, classList: ['positive']});
+
+ s1.customContextRange = tr.b.math.Range.fromExplicitRange(0, 150);
+ checkSparkline(s1, {left: 0, width: 34.33, classList: ['positive']});
+ checkSparkline(s2, {left: 0, width: 41, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 76, classList: ['positive']});
+ checkSparkline(s4, {left: 0, width: 101, classList: ['positive']});
+
+ s4.contextGroup = null;
+ checkSparkline(s1, {left: 0, width: 34.33, classList: ['positive']});
+ checkSparkline(s2, {left: 0, width: 41, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+
+ s1.customContextRange = undefined;
+ checkSparkline(s1, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s2, {left: 0, width: 41, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+
+ s4.value = 0;
+ checkSparkline(s1, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s2, {left: 0, width: 41, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+
+ div.removeChild(s1);
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+
+ s1.contextGroup = 'B';
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+
+ div.appendChild(s1);
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 61, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+ checkSparkline(s1, {left: 0, width: 101, classList: ['positive']});
+
+ s1.removeAttribute('context-group');
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+ checkSparkline(s1, {display: 'none'});
+
+ s1.customContextRange = tr.b.math.Range.fromExplicitRange(0, 100);
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s4, {display: 'none'});
+ checkSparkline(s1, {left: 0, width: 51, classList: ['positive']});
+
+ s3.value = 0;
+ checkSparkline(s2, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(s3, {display: 'none'});
+ checkSparkline(s4, {display: 'none'});
+ checkSparkline(s1, {left: 0, width: 51, classList: ['positive']});
+ });
+
+ test('deltaSparkline_noImprovementDirection', function() {
+ const div = document.createElement('div');
+ div.style.width = '101px'; // One extra pixel for sparkline border.
+ this.addHTMLOutput(div);
+ div.appendChild(
+ document.createElement('tr-v-ui-scalar-context-controller'));
+
+ const span1 = tr.v.ui.createScalarSpan(20971520, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span1);
+ const span2 = tr.v.ui.createScalarSpan(15728640, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span2);
+ const span3 = tr.v.ui.createScalarSpan(12582912, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span3);
+ const span4 = tr.v.ui.createScalarSpan(11534336, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span4);
+ const span5 = tr.v.ui.createScalarSpan(10485760, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span5);
+ const span6 = tr.v.ui.createScalarSpan(9437184, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span6);
+ const span7 = tr.v.ui.createScalarSpan(8388608, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span7);
+ const span8 = tr.v.ui.createScalarSpan(5242880, {
+ unit: Unit.byName.sizeInBytesDelta,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span8);
+
+ // We must check the sparklines *after* all spans are appended because new
+ // values can change the context range.
+ checkSparkline(span1, {left: 0, width: 101, classList: ['positive']});
+ checkSparkline(span2, {left: 0, width: 76, classList: ['positive']});
+ checkSparkline(span3, {left: 0, width: 61, classList: ['positive']});
+ checkSparkline(span4, {left: 0, width: 56, classList: ['positive']});
+ checkSparkline(span5, {left: 0, width: 51, classList: ['positive']});
+ checkSparkline(span6, {left: 0, width: 46, classList: ['positive']});
+ checkSparkline(span7, {left: 0, width: 41, classList: ['positive']});
+ checkSparkline(span8, {left: 0, width: 26, classList: ['positive']});
+ });
+
+ test('deltaSparkline_smallerIsBetter', function() {
+ const div = document.createElement('div');
+ div.style.width = '101px'; // One extra pixel for sparkline border.
+ this.addHTMLOutput(div);
+ div.appendChild(
+ document.createElement('tr-v-ui-scalar-context-controller'));
+
+ const span1 = tr.v.ui.createScalarSpan(5242880, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span1);
+ const span2 = tr.v.ui.createScalarSpan(0, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span2);
+ const span3 = tr.v.ui.createScalarSpan(-3145728, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span3);
+ const span4 = tr.v.ui.createScalarSpan(-4194304, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span4);
+ const span5 = tr.v.ui.createScalarSpan(-5242880, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span5);
+ const span6 = tr.v.ui.createScalarSpan(-6291456, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span6);
+ const span7 = tr.v.ui.createScalarSpan(-7340032, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span7);
+ const span8 = tr.v.ui.createScalarSpan(-15728640, {
+ unit: Unit.byName.sizeInBytesDelta_smallerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span8);
+
+ // We must check the sparklines *after* all spans are appended because new
+ // values can change the context range.
+ checkSparkline(span1,
+ {left: 75, width: 26, classList: ['positive', 'worse']});
+ checkSparkline(span2, {left: 75, width: 1, classList: ['same']});
+ checkSparkline(span3, {left: 60, width: 16, classList: ['better']});
+ checkSparkline(span4, {left: 55, width: 21, classList: ['better']});
+ checkSparkline(span5, {left: 50, width: 26, classList: ['better']});
+ checkSparkline(span6, {left: 45, width: 31, classList: ['better']});
+ checkSparkline(span7, {left: 40, width: 36, classList: ['better']});
+ checkSparkline(span8, {left: 0, width: 76, classList: ['better']});
+ });
+
+ test('deltaSparkline_biggerIsBetter', function() {
+ const div = document.createElement('div');
+ div.style.width = '101px'; // One extra pixel for sparkline border.
+ this.addHTMLOutput(div);
+ div.appendChild(
+ document.createElement('tr-v-ui-scalar-context-controller'));
+
+ const span1 = tr.v.ui.createScalarSpan(0, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span1);
+ const span2 = tr.v.ui.createScalarSpan(-5242880, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span2);
+ const span3 = tr.v.ui.createScalarSpan(-8388608, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span3);
+ const span4 = tr.v.ui.createScalarSpan(-9437184, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span4);
+ const span5 = tr.v.ui.createScalarSpan(-10485760, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span5);
+ const span6 = tr.v.ui.createScalarSpan(-11534336, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span6);
+ const span7 = tr.v.ui.createScalarSpan(-12582912, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span7);
+ const span8 = tr.v.ui.createScalarSpan(-20971520, {
+ unit: Unit.byName.sizeInBytesDelta_biggerIsBetter,
+ contextGroup: 'test'
+ });
+ Polymer.dom(div).appendChild(span8);
+
+ // We must check the sparklines *after* all spans are appended because new
+ // values can change the context range.
+ checkSparkline(span1, {left: 100, width: 1, classList: ['same']});
+ checkSparkline(span2, {left: 75, width: 26, classList: ['worse']});
+ checkSparkline(span3, {left: 60, width: 41, classList: ['worse']});
+ checkSparkline(span4, {left: 55, width: 46, classList: ['worse']});
+ checkSparkline(span5, {left: 50, width: 51, classList: ['worse']});
+ checkSparkline(span6, {left: 45, width: 56, classList: ['worse']});
+ checkSparkline(span7, {left: 40, width: 61, classList: ['worse']});
+ checkSparkline(span8, {left: 0, width: 101, classList: ['worse']});
+ });
+
+ test('classListChanges', function() {
+ const div = document.createElement('div');
+ div.style.width = '200px';
+ this.addHTMLOutput(div);
+
+ const span = tr.v.ui.createScalarSpan(10, {
+ unit: Unit.byName.energyInJoulesDelta_smallerIsBetter,
+ significance: tr.b.math.Statistics.Significance.SIGNIFICANT,
+ customContextRange: tr.b.math.Range.fromExplicitRange(-20, 20)
+ });
+ Polymer.dom(div).appendChild(span);
+
+ assert.sameMembers(Array.from(span.$.content.classList), ['worse']);
+ checkSignificance(span, 'significantly_worse');
+
+ span.significance = tr.b.math.Statistics.Significance.DONT_CARE;
+ assert.sameMembers(Array.from(span.$.sparkline.classList),
+ ['positive', 'worse']);
+ assert.sameMembers(Array.from(span.$.content.classList), ['worse']);
+ checkSignificance(span, '');
+
+ span.value = -5;
+ assert.sameMembers(Array.from(span.$.sparkline.classList), ['better']);
+ assert.sameMembers(Array.from(span.$.content.classList), ['better']);
+ checkSignificance(span, '');
+
+ span.unit = Unit.byName.energyInJoules;
+ assert.sameMembers(Array.from(span.$.sparkline.classList), []);
+ assert.sameMembers(Array.from(span.$.content.classList), []);
+ checkSignificance(span, '');
+
+ span.value = 20;
+ assert.sameMembers(Array.from(span.$.sparkline.classList), ['positive']);
+ assert.sameMembers(Array.from(span.$.content.classList), []);
+ checkSignificance(span, '');
+
+ span.unit = Unit.byName.energyInJoulesDelta_biggerIsBetter;
+ assert.sameMembers(Array.from(span.$.sparkline.classList),
+ ['positive', 'better']);
+ assert.sameMembers(Array.from(span.$.content.classList), ['better']);
+ checkSignificance(span, '');
+
+ span.significance = tr.b.math.Statistics.Significance.INSIGNIFICANT;
+ assert.sameMembers(Array.from(span.$.sparkline.classList),
+ ['positive', 'better']);
+ assert.sameMembers(Array.from(span.$.content.classList), ['better']);
+ checkSignificance(span, 'insignificant');
+
+ span.unit = Unit.byName.energyInJoulesDelta_smallerIsBetter;
+ assert.sameMembers(Array.from(span.$.sparkline.classList),
+ ['positive', 'worse']);
+ assert.sameMembers(Array.from(span.$.content.classList), ['worse']);
+ checkSignificance(span, 'insignificant');
+
+ span.unit = Unit.byName.energyInJoulesDelta;
+ assert.sameMembers(Array.from(span.$.sparkline.classList), ['positive']);
+ assert.sameMembers(Array.from(span.$.content.classList), []);
+ checkSignificance(span, '');
+
+ span.value = 0;
+ assert.sameMembers(Array.from(span.$.sparkline.classList), []);
+ assert.sameMembers(Array.from(span.$.content.classList), []);
+ checkSignificance(span, '');
+ });
+
+ test('sparkline_uncentered', function() {
+ const div = document.createElement('div');
+ this.addHTMLOutput(div);
+ div.appendChild(
+ document.createElement('tr-v-ui-scalar-context-controller'));
+
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(-1, {
+ unit: Unit.byName.powerInWattsDelta,
+ contextGroup: 'test'
+ }));
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(100, {
+ unit: Unit.byName.powerInWattsDelta,
+ contextGroup: 'test'
+ }));
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(80, {
+ unit: Unit.byName.powerInWattsDelta,
+ contextGroup: 'test'
+ }));
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(60, {
+ unit: Unit.byName.powerInWattsDelta,
+ contextGroup: 'test'
+ }));
+ });
+
+ test('sparkline_centered', function() {
+ const div = document.createElement('div');
+ this.addHTMLOutput(div);
+ div.appendChild(
+ document.createElement('tr-v-ui-scalar-context-controller'));
+
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(-1, {
+ unit: Unit.byName.powerInWattsDelta,
+ customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100)
+ }));
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(100, {
+ unit: Unit.byName.powerInWattsDelta,
+ customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100)
+ }));
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(80, {
+ unit: Unit.byName.powerInWattsDelta,
+ customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100)
+ }));
+ Polymer.dom(div).appendChild(tr.v.ui.createScalarSpan(60, {
+ unit: Unit.byName.powerInWattsDelta,
+ customContextRange: tr.b.math.Range.fromExplicitRange(-100, 100)
+ }));
+ });
+
+ timedPerfTest('memory_scalar_spans', function() {
+ tr.v.ui.createScalarSpan(EXAMPLE_MEMORY_NUMERIC, {
+ context: EXAMPLE_MEMORY_FORMATTING_CONTEXT,
+ inline: true,
+ });
+ }, {
+ iterations: 1000,
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/timings.md b/chromium/third_party/catapult/tracing/tracing/value/ui/timings.md
new file mode 100644
index 00000000000..3846af6ee0d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/timings.md
@@ -0,0 +1,78 @@
+This document describes the Google Analytics metrics reported by results.html.
+
+Measures are recorded by the performance web API, and are visible in the User
+Timing track in the devtools Performance timeline.
+
+Instant events are recorded using console.timestamp(), and are visible as orange
+ticks at the top of the devtools Performance timeline.
+
+Both measures and instant events are recorded as Events in Google Analytics.
+[Access these metrics here](https://analytics.google.com/analytics/web/#embed/report-home/a98760012w145165698p149871853/) if you have been granted access.
+
+ * histogram-set-controls
+ * `alpha` measures response latency of changing the statistical significance
+ threshold, alpha.
+ * `hideOverviewCharts` measures response latency of hiding all overview
+ charts.
+ * `referenceColumn` measures response latency of changing the reference
+ column.
+ * `search` measures response latency of changing the search query to filter
+ rows.
+ * `showAll` measures response latency of toggling showing all rows versus
+ source Histograms only.
+ * `showOverviewCharts` measures response latency of showing all overview
+ charts.
+ * `statistic` measures response latency of changing the statistic that is
+ displayed in histogram-set-table-cells.
+ * HistogramSetLocation
+ * `onPopState` measures response latency of the browser back button.
+ * `pushState` measures latency of serializing the view state and pushing it
+ to the HTML5 history API. This happens automatically whenever any part of
+ the ViewState is updated.
+ * histogram-set-table
+ * `columnCount` instant event contains the number of columns, recorded when the
+ table is built.
+ * `nameColumnConstrained` instant event recorded when the name column width
+ is constrained.
+ * `nameColumnUnconstrained` instant event recorded when the name column width
+ is unconstrained.
+ * `rootRowCount` instant event contains the number of root rows, recorded
+ whenever it changes or the table is built.
+ * `rowCollapsed` instant event recorded whenever a row is collapsed.
+ * `rowExpanded` instant event recorded whenever a row is expanded.
+ * `selectHistogramNames` instant event recorded whenever a breakdown related
+ histogram name link is clicked.
+ * `sortColumn` instant event recorded whenever the user changes the sort
+ column.
+ * histogram-set-table-cell
+ * `close` instant event recorded when the cell is closed.
+ * `open` instant event recorded when the cell is opened.
+ * histogram-set-table-name-cell
+ * `closeHistograms` instant event recorded when the user clicks the button to
+ close all histogram-set-table-cells in the row.
+ * `hideOverview` instant event recorded when the user clicks the button to
+ hide the overview line charts for the row.
+ * `openHistograms` instant event recorded when the user clicks the button to
+ open all histogram-set-table-cells in the row.
+ * `showOverview` instant event recorded when the user clicks the button to
+ show the overview line charts for the row.
+ * histogram-set-view
+ * `build` measures latency to find source Histograms, collect parameters,
+ configure the controls and build the table. Does not include parsing
+ Histograms from json.
+ * `sourceHistograms` measures latency to find source Histograms in the
+ relationship graphical model.
+ * `collectParameters` measures latency to collect display labels, statistic
+ names, and possible groupings.
+ * `export{Raw,Merged}{CSV,JSON}` measures latency to download a CSV/JSON file
+ of raw/merged Histograms.
+ * histogram-span
+ * `brushBins` instant event recorded when the user finishes brushing bins.
+ * `clearBrushedBins` instant event recorded when the user clears brushed
+ bins.
+ * `mergeSampleDiagnostics` measures latency of displaying the table of merged
+ sample diagnostics.
+ * `splitSampleDiagnostics` measures latency of displaying the table of
+ unmerged sample diagnostics.
+ * HistogramParameterCollector
+ * `maxSampleCount` instant event records maximum Histogram.numValues
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html
new file mode 100644
index 00000000000..de68e0cfa57
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<!--
+ This file only depends on diagnostic_span.html, but it must be imported from
+ diagnostic_span.html.
+ Fortunately, this file is only imported from diagnostic_span.html, so it can
+ just not import anything.
+-->
+
+<link rel="import" href="/tracing/value/ui/diagnostic_span_behavior.html">
+
+<dom-module id="tr-v-ui-unmergeable-diagnostic-set-span">
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.v.ui', function() {
+ Polymer({
+ is: 'tr-v-ui-unmergeable-diagnostic-set-span',
+ behaviors: [tr.v.ui.DIAGNOSTIC_SPAN_BEHAVIOR],
+
+ updateContents_() {
+ Polymer.dom(this).textContent = '';
+ for (const diagnostic of this.diagnostic) {
+ if (diagnostic instanceof tr.v.d.RelatedNameMap) continue;
+ const div = document.createElement('div');
+ div.appendChild(tr.v.ui.createDiagnosticSpan(
+ diagnostic, this.name_, this.histogram_));
+ Polymer.dom(this).appendChild(div);
+ }
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html
new file mode 100644
index 00000000000..29e48966fc0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/unmergeable_diagnostic_set_span_test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html">
+<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const event = tr.c.TestUtils.newSliceEx({
+ title: 'event',
+ start: 0,
+ duration: 1,
+ });
+ event.parentContainer = {
+ sliceGroup: {
+ stableId: 'fake_thread',
+ slices: [event],
+ },
+ };
+ const diagnostics = new tr.v.d.UnmergeableDiagnosticSet([
+ new tr.v.d.GenericSet(['generic diagnostic']),
+ new tr.v.d.RelatedNameMap(),
+ new tr.v.d.RelatedEventSet([
+ event,
+ ]),
+ ]);
+ const span = tr.v.ui.createDiagnosticSpan(diagnostics);
+ assert.strictEqual('TR-V-UI-UNMERGEABLE-DIAGNOSTIC-SET-SPAN', span.tagName);
+ this.addHTMLOutput(span);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html
new file mode 100644
index 00000000000..0adeaf55e3c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container.html
@@ -0,0 +1,410 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2018 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/value/ui/metrics_visualization.html">
+<link rel="import" href="/tracing/value/ui/raster_visualization.html">
+<meta charset="utf-8">
+<dom-module id='tr-v-ui-visualizations-data-container'>
+ <template>
+ <style>
+ .error {
+ color: red;
+ display: none;
+ }
+
+ .sample{
+ display: none;
+ }
+
+ .subtitle{
+ font-size: 20px;
+ font-weight: bold;
+ padding-bottom: 5px;
+ }
+
+ .description{
+ font-size: 15px;
+ padding-bottom: 5px;
+ }
+
+ #title {
+ font-size: 30px;
+ font-weight: bold;
+ padding-bottom: 5px;
+ }
+ </style>
+ <div id="title">Visualizations</div>
+ <div id="data_error" class="error">Invalid data provided.</div>
+ <div id="pipeline_per_frame_container">
+ <div class="subtitle">Graphics Pipeline and Raster Tasks</div>
+ <div class="description">
+ When raster tasks are completed in comparison to the rest of the graphics pipeline.<br>
+ Only pages where raster tasks are completed after beginFrame is issued are included.
+ </div>
+ <tr-v-ui-raster-visualization id="rasterVisualization">
+ </tr-v-ui-raster-visualization>
+ </div>
+ <div id=metrics_container>
+ <div class="subtitle">Metrics</div>
+ <div class="description">Total amount of time taken for the indicated metrics.</div>
+ <tr-v-ui-metrics-visualization id="metricsVisualization" class="sample">
+ </tr-v-ui-metrics-visualization>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.v.ui', function() {
+ const STATISTICS_KEY = 'statistics';
+ const SUBMETRICS_KEY = 'submetrics';
+ const AGGREGATE_KEY = 'aggregate';
+ const RASTER_START_METRIC_KEY = 'pipeline:begin_frame_to_raster_start';
+
+ const COLORS = [
+ ['#FFD740', '#FFC400', '#FFAB00', '#E29800'],
+ ['#FF6E40', '#FF3D00', '#DD2C00', '#A32000'],
+ ['#40C4FF', '#00B0FF', '#0091EA', '#006DAF'],
+ ['#89C641', '#54B503', '#4AA510', '#377A0D'],
+ ['#B388FF', '#7C4DFF', '#651FFF', '#6200EA'],
+ ['#FF80AB', '#FF4081', '#F50057', '#C51162'],
+ ['#FFAB40', '#FF9100', '#FF6D00', '#D65C02'],
+ ['#8C9EFF', '#536DFE', '#3D5AFE', '#304FFE']];
+
+ const FRAME = [new Map([
+ ['pipeline:begin_frame_to_raster_start', false],
+ ['pipeline:begin_frame_to_raster_end', true]]), new Map([
+ ['pipeline:begin_frame_transport', true],
+ ['pipeline:begin_frame_to_frame_submission', true],
+ ['pipeline:frame_submission_to_display', true],
+ ['pipeline:draw', true]])];
+
+ const METRICS = new Map([
+ ['Pipeline', [
+ 'pipeline:begin_frame_transport',
+ 'pipeline:begin_frame_to_frame_submission',
+ 'pipeline:frame_submission_to_display',
+ 'pipeline:draw']],
+ ['Thread', [
+ 'thread_browser_cpu_time_per_frame',
+ 'thread_display_compositor_cpu_time_per_frame',
+ 'thread_GPU_cpu_time_per_frame',
+ 'thread_IO_cpu_time_per_frame',
+ 'thread_other_cpu_time_per_frame',
+ 'thread_raster_cpu_time_per_frame',
+ 'thread_renderer_compositor_cpu_time_per_frame',
+ 'thread_renderer_main_cpu_time_per_frame']]]);
+
+ function getValueFromMap(key, map) {
+ let retrievedValue = map.get(key);
+ if (!retrievedValue) {
+ retrievedValue = new Map();
+ map.set(key, retrievedValue);
+ }
+ return retrievedValue;
+ }
+
+ Polymer({
+ is: 'tr-v-ui-visualizations-data-container',
+
+ created() {
+ // from earliest to latest
+ this.orderedBenchmarks_ = [];
+ // aggregate/page -> benchmark -> metric -> statistics/submetrics
+ this.groupedData_ = new Map();
+ },
+
+ build(leafHistograms, histograms) {
+ if (!leafHistograms || leafHistograms.length < 1 ||
+ !histograms || histograms.length < 1) {
+ this.$.data_error.style.display = 'block';
+ return;
+ }
+
+ this.processHistograms_(this.groupHistograms_(histograms),
+ this.groupHistograms_(leafHistograms));
+ this.buildCharts_();
+ },
+
+ processHistograms_(histograms, leafHistograms) {
+ const benchmarkStartGrouping = tr.v.HistogramGrouping.BY_KEY.get(
+ tr.v.d.RESERVED_NAMES.BENCHMARK_START);
+
+ const benchmarkToStartTime = new Map();
+ for (const [metric, benchmarks] of histograms.entries()) {
+ // process aggregate data
+ for (const [benchmark, pages] of leafHistograms.get(metric).entries()) {
+ for (const [page, histograms] of pages.entries()) {
+ for (const histogram of histograms) {
+ const aggregateToBenchmarkMap = getValueFromMap(AGGREGATE_KEY,
+ this.groupedData_);
+ const benchmarkToMetricMap = getValueFromMap(benchmark,
+ aggregateToBenchmarkMap);
+
+ benchmarkToMetricMap.set(metric, new Map([
+ [STATISTICS_KEY, histogram.running]]));
+ }
+ }
+ }
+
+ // process data per page
+ for (const [benchmark, pages] of benchmarks.entries()) {
+ for (const [page, histograms] of pages.entries()) {
+ for (const histogram of histograms) {
+ if (!benchmarkToStartTime.get(benchmark)) {
+ benchmarkToStartTime.set(benchmark,
+ benchmarkStartGrouping.callback(histogram));
+ }
+
+ const pageToBenchmarkMap = getValueFromMap(page,
+ this.groupedData_);
+ const benchmarkToMetricMap = getValueFromMap(benchmark,
+ pageToBenchmarkMap);
+
+ // retrieving submetric _ta
+ const mergedSubmetrics = new tr.v.d.DiagnosticMap();
+ for (const bin of histogram.allBins) {
+ for (const map of bin.diagnosticMaps) {
+ mergedSubmetrics.addDiagnostics(map);
+ }
+ }
+
+ if (benchmarkToMetricMap.get(metric)) continue;
+ benchmarkToMetricMap.set(metric, new Map([
+ [STATISTICS_KEY, histogram.running],
+ [SUBMETRICS_KEY, mergedSubmetrics.get('breakdown')]]));
+ }
+ }
+ }
+ }
+ this.orderedBenchmarks_ = this.sortBenchmarks_(benchmarkToStartTime);
+ },
+
+ groupHistograms_(histograms) {
+ const groupings = [
+ tr.v.HistogramGrouping.HISTOGRAM_NAME,
+ tr.v.HistogramGrouping.DISPLAY_LABEL,
+ tr.v.HistogramGrouping.BY_KEY.get(tr.v.d.RESERVED_NAMES.STORIES)];
+
+ return histograms.groupHistogramsRecursively(groupings);
+ },
+
+ sortBenchmarks_(benchmarks) {
+ return Array.from(benchmarks.keys()).sort((a, b) => {
+ Date.parse(benchmarks.get(a)) - Date.parse(benchmarks.get(b));
+ });
+ },
+
+ getSeriesKey_(metric, benchmark) {
+ return metric + '-' + benchmark;
+ },
+
+ buildCharts_() {
+ const rasterDataToBePassed = this.buildRasterChart_();
+ this.$.rasterVisualization.build(rasterDataToBePassed);
+
+ for (const chartName of METRICS.keys()) {
+ const metricsDataToBePassed = this.buildMetricsData_(chartName);
+ const newChart = this.$.metricsVisualization.cloneNode(true);
+ newChart.style.display = 'block';
+ Polymer.dom(this.$.metrics_container).appendChild(newChart);
+ newChart.build(metricsDataToBePassed);
+ }
+ },
+
+ buildRasterChart_() {
+ const orderedPages = [...this.groupedData_.keys()]
+ .filter((page) => this.filterPagesWithoutRasterMetric_(page))
+ .sort((a, b) => this.sortByRasterStart_(a, b));
+ const allChartData = new Map();
+ for (const page of orderedPages) {
+ const pageMap = this.groupedData_.get(page);
+ let chartData = [];
+ for (const benchmark of this.orderedBenchmarks_) {
+ if (!pageMap.has(benchmark)) continue;
+ const benchmarkMap = pageMap.get(benchmark);
+ const benchmarkData = [];
+ if (benchmarkMap.get(RASTER_START_METRIC_KEY) === undefined) {
+ continue;
+ }
+ for (const [threadName, thread] of FRAME.entries()) {
+ const data = {x: benchmark, hide: 0};
+ if (page !== AGGREGATE_KEY) data.group = page;
+ let rasterBegin = 0;
+ for (const metric of thread.keys()) {
+ const metricMap = benchmarkMap.get(metric);
+ const key = this.getSeriesKey_(metric,
+ data.x + '-' + threadName);
+ const stats = metricMap.get(STATISTICS_KEY);
+ const mean = stats ? stats.mean : 0;
+ let roundedMean = Math.round(mean * 100) / 100;
+ if (metric === RASTER_START_METRIC_KEY) {
+ rasterBegin = roundedMean;
+ } else if (metric === 'pipeline:begin_frame_to_raster_end') {
+ roundedMean -= rasterBegin;
+ }
+ data[key] = roundedMean;
+ }
+ benchmarkData.push(data);
+ }
+ chartData = chartData.concat(benchmarkData);
+ }
+ allChartData.set(page, chartData);
+ }
+ return allChartData;
+ },
+
+ buildMetricsData_(chartName) {
+ // pages are ordered from smallest to largest by their total
+ // values for the first benchmark
+ const orderedPages = [...this.groupedData_.keys()].sort((a, b) =>
+ this.sortByTotal_(a, b, chartName));
+ const chartData = [];
+ const aggregateChart = [];
+ for (const page of orderedPages) {
+ const pageMap = this.groupedData_.get(page);
+ for (const benchmark of this.orderedBenchmarks_) {
+ if (!pageMap.has(benchmark)) continue;
+ const data = {x: benchmark, group: page};
+ const benchmarkMap = pageMap.get(benchmark);
+ for (const metric of METRICS.get(chartName)) {
+ const metricMap = benchmarkMap.get(metric);
+ const key = this.getSeriesKey_(metric, benchmark);
+ const stats = metricMap.get(STATISTICS_KEY);
+ const mean = stats ? stats.mean : 0;
+ data[key] = Math.round(mean * 100) / 100;
+ }
+ if (page === AGGREGATE_KEY) {
+ aggregateChart.push(data);
+ } else {
+ chartData.push(data);
+ }
+ }
+ chartData.push({});
+ }
+ chartData.shift();
+ return {
+ title: chartName,
+ aggregate: aggregateChart,
+ page: chartData,
+ submetrics: this.processSubmetricsData_(chartName)
+ };
+ },
+
+ submetricsHelper_(submetric, value, benchmark, metricToSubmetricMap) {
+ let submetricToBenchmarkMap = metricToSubmetricMap.get(submetric);
+ if (!submetricToBenchmarkMap) {
+ submetricToBenchmarkMap = [];
+ metricToSubmetricMap.set(submetric, submetricToBenchmarkMap);
+ }
+ const data = {x: submetric, hide: 0, group: benchmark};
+ const mean = value;
+ const roundedMean = Math.round(mean * 100) / 100;
+ if (!roundedMean) return;
+ data[this.getSeriesKey_(submetric, benchmark)] = roundedMean;
+ submetricToBenchmarkMap.push(data);
+ },
+
+ // Get data for breakdown of a main step
+ processSubmetricsData_(chartName) {
+ // page -> metric -> submetric ->
+ // array of submetrics across all benchmarks
+ const submetrics = new Map();
+ for (const [page, pageMap] of this.groupedData_.entries()) {
+ if (page === AGGREGATE_KEY) continue;
+ const pageToMetricMap = getValueFromMap(page, submetrics);
+ for (const benchmark of this.orderedBenchmarks_) {
+ const benchmarkMap = pageMap.get(benchmark);
+ if (!benchmarkMap) continue;
+ for (const metric of METRICS.get(chartName)) {
+ const metricMap = benchmarkMap.get(metric);
+ const metricToSubmetricMap = getValueFromMap(metric,
+ pageToMetricMap);
+ const submetrics = metricMap.get(SUBMETRICS_KEY);
+ if (!submetrics) {
+ this.submetricsHelper_(metric, metricMap.get(STATISTICS_KEY),
+ benchmark, metricToSubmetricMap);
+ continue;
+ }
+ for (const [submetric, value] of [...submetrics]) {
+ this.submetricsHelper_(submetric, value, benchmark,
+ metricToSubmetricMap);
+ }
+ }
+ }
+ }
+ return submetrics;
+ },
+
+ sortByTotal_(a, b, chartName) {
+ if (a === AGGREGATE_KEY) return -1;
+ if (b === AGGREGATE_KEY) return 1;
+ let aValue = 0;
+ const aMap = this.groupedData_.get(a);
+ if (aMap.get(this.orderedBenchmarks_[0]) !== undefined) {
+ for (const metric of METRICS.get(chartName)) {
+ const aMetricMap = aMap.get(this.orderedBenchmarks_[0]).get(metric);
+ const aStats = aMetricMap.get(STATISTICS_KEY);
+ aValue += aStats ? aStats.mean : 0;
+ }
+ }
+ let bValue = 0;
+ const bMap = this.groupedData_.get(b);
+ if (bMap.get(this.orderedBenchmarks_[0]) !== undefined) {
+ for (const metric of METRICS.get(chartName)) {
+ const bMetricMap = bMap.get(this.orderedBenchmarks_[0]).get(metric);
+ const bStats = bMetricMap.get(STATISTICS_KEY);
+ bValue += bStats ? bStats.mean : 0;
+ }
+ }
+ return aValue - bValue;
+ },
+
+ filterPagesWithoutRasterMetric_(page) {
+ const pageMap = this.groupedData_.get(page);
+ for (const benchmark of this.orderedBenchmarks_) {
+ const pageMetricMap = pageMap.get(benchmark);
+ if (!pageMetricMap) continue;
+ const wantedMetric = pageMetricMap.get(RASTER_START_METRIC_KEY);
+ if (wantedMetric !== undefined) return true;
+ }
+ return false;
+ },
+
+ sortByRasterStart_(a, b) {
+ if (a === AGGREGATE_KEY) return 1;
+ if (b === AGGREGATE_KEY) return -1;
+ let aValue = 0;
+ const aMap = this.groupedData_.get(a);
+ if (aMap.get(this.orderedBenchmarks_[0]) !== undefined) {
+ const aMetricMap = aMap.get(this.orderedBenchmarks_[0])
+ .get(RASTER_START_METRIC_KEY);
+ const aStats = aMetricMap.get(STATISTICS_KEY);
+ aValue = aStats ? aStats.mean : 0;
+ }
+ let bValue = 0;
+ const bMap = this.groupedData_.get(b);
+ if (bMap.get(this.orderedBenchmarks_[0]) !== undefined) {
+ const bMetricMap = bMap.get(this.orderedBenchmarks_[0])
+ .get(RASTER_START_METRIC_KEY);
+ const bStats = bMetricMap.get(STATISTICS_KEY);
+ bValue = bStats ? bStats.mean : 0;
+ }
+ return bValue - aValue;
+ },
+ });
+
+ return {
+ STATISTICS_KEY,
+ SUBMETRICS_KEY,
+ AGGREGATE_KEY,
+ COLORS,
+ FRAME,
+ METRICS,
+ getValueFromMap,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html
new file mode 100644
index 00000000000..1199d8ed731
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/value/ui/visualizations_data_container_test.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2018 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/visualizations_data_container.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function getHistogram(name) {
+ const samples = [];
+ for (let i = 0; i < 5; ++i) {
+ const total = Math.random();
+ const values = {};
+ values[name + 'a'] = total / 2.0;
+ values[name + 'b'] = total / 4.0;
+ values[name + 'c'] = total / 4.0;
+ samples.push({
+ value: total,
+ diagnostics: new Map([
+ [
+ tr.v.d.RESERVED_NAMES.BENCHMARK_START,
+ new tr.v.d.DateRange(Date.now()),
+ ], [
+ 'breakdown', tr.v.d.Breakdown.fromDict({values}),
+ ],
+ ]),
+ });
+ }
+ return tr.v.Histogram.create(name, tr.b.Unit.byName.count, samples);
+ }
+
+ function getHistogramSet(displayLabel, story, containsRasterStart = true) {
+ const histograms = new tr.v.HistogramSet();
+ let metrics = [];
+ for (const category of tr.v.ui.METRICS.values()) {
+ metrics = metrics.concat(category);
+ }
+ for (const metric of metrics) {
+ histograms.addHistogram(getHistogram(metric));
+ }
+
+ if (containsRasterStart) {
+ histograms.addHistogram(
+ getHistogram('pipeline:begin_frame_to_raster_start'));
+ histograms.addHistogram(
+ getHistogram('pipeline:begin_frame_to_raster_end'));
+ }
+ histograms.addSharedDiagnosticToAllHistograms(
+ tr.v.d.RESERVED_NAMES.LABELS, new tr.v.d.GenericSet([displayLabel]));
+ histograms.addSharedDiagnosticToAllHistograms(
+ tr.v.d.RESERVED_NAMES.STORIES, new tr.v.d.GenericSet([story]));
+ return histograms;
+ }
+
+ test('instantiate', function() {
+ const cp = document.createElement('tr-v-ui-visualizations-data-container');
+ this.addHTMLOutput(cp);
+
+ const histograms = getHistogramSet('Run 1', 'test.com');
+
+ const histograms2 = getHistogramSet('Run 2', 'test.com');
+ histograms.importDicts(histograms2.asDicts());
+
+ const histograms3 = getHistogramSet('Run 1', 'abc.com');
+ histograms.importDicts(histograms3.asDicts());
+
+ const histograms4 = getHistogramSet('Run 2', 'abc.com');
+ histograms.importDicts(histograms4.asDicts());
+
+ cp.build(histograms, histograms);
+ });
+
+ test('instantiateWithRepeat', function() {
+ const cp = document.createElement('tr-v-ui-visualizations-data-container');
+ this.addHTMLOutput(cp);
+
+ const histograms = getHistogramSet('Run 1', 'repeat.com');
+ const histograms2 = getHistogramSet('Run 1', 'repeat.com');
+ histograms.importDicts(histograms2.asDicts());
+
+ cp.build(histograms, histograms);
+ });
+
+ test('instantiateWithoutRasterTasks', function() {
+ const cp = document.createElement('tr-v-ui-visualizations-data-container');
+ this.addHTMLOutput(cp);
+
+ const histograms = getHistogramSet('Run 1', 'test.com', false);
+
+ const histograms2 = getHistogramSet('Run 2', 'test.com', false);
+ histograms.importDicts(histograms2.asDicts());
+
+ const histograms3 = getHistogramSet('Run 1', 'abc.com');
+ histograms.importDicts(histograms3.asDicts());
+
+ const histograms4 = getHistogramSet('Run 2', 'abc.com');
+ histograms.importDicts(histograms4.asDicts());
+
+ cp.build(histograms, histograms);
+ });
+
+ test('instantiateWithDifferentStorySets', function() {
+ const cp = document.createElement('tr-v-ui-visualizations-data-container');
+ this.addHTMLOutput(cp);
+
+ const histograms = getHistogramSet('Run 1', 'test.com');
+
+ const histograms2 = getHistogramSet('Run 1', 'abc.com');
+ histograms.importDicts(histograms2.asDicts());
+
+ const histograms3 = getHistogramSet('Run 2', 'abc.com');
+ histograms.importDicts(histograms3.asDicts());
+
+ cp.build(histograms, histograms);
+ });
+});
+</script>