blob: c0382f66b93f298852bd249d200a966dccf8ae74 [file] [log] [blame]
<!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>