| <!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> |