blob: 4eb50e885fa2fbd4b6402bb4dbbf3b495affb564 [file] [log] [blame]
<!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/table.html">
<link rel="import" href="/tracing/value/ui/diagnostic_span.html">
<link rel="import" href="/tracing/value/ui/histogram_span.html">
<link rel="import" href="/tracing/value/ui/scalar_span.html">
<link rel="import" href="/tracing/value/ui/value_set_view.html">
<link rel="import" href="/tracing/value/unit.html">
<dom-module id="tr-v-ui-value-set-table">
<template>
<style>
:host {
display: flex;
flex-direction: column;
}
table-container {
display: flex;
min-height: 0px;
overflow-y: auto;
}
div#error {
color: red;
}
#histogram {
display: none;
}
#search {
max-width: 20em;
margin-top: 5px;
margin-bottom: 5px;
}
</style>
<input id="search" placeholder="Find Value name">
<div>
<input type="checkbox" id="show_all">
<label for="show_all">Show all</label>
</div>
<div id="error"></div>
<table-container>
<tr-ui-b-table id="table"></tr-ui-b-table>
</table-container>
<tr-v-ui-histogram-span id="histogram"></tr-v-ui-histogram-span>
</template>
</dom-module>
<script>
'use strict';
tr.exportTo('tr.ui', function() {
/**
* @param {!tr.v.Value} value
* @param {string} fieldName
* @param {*} defaultValue
* @return {*}
*/
function getIterationInfoField(value, fieldName, defaultValue) {
var iteration = tr.v.d.IterationInfo.getFromValue(value);
if (!(iteration instanceof tr.v.d.IterationInfo))
return defaultValue;
return iteration[fieldName];
}
/**
* @param {!tr.v.Value} value
* @param {string} fieldName
* @return {string}
*/
function getStoryGroupingKeyLabel(value, storyGroupingKey) {
var iteration = tr.v.d.IterationInfo.getFromValue(value);
if (!(iteration instanceof tr.v.d.IterationInfo))
return storyGroupingKey + ': undefined';
return storyGroupingKey + ': ' +
iteration.storyGroupingKeys[storyGroupingKey];
}
/**
* @param {!tr.v.Value} value
* @return {string}
*/
var getDisplayLabel = v => getIterationInfoField(v, 'displayLabel', 'Value');
var SELECTED_VALUE_SETTINGS_KEY = 'tr-v-ui-value-set-table-value';
var SHOW_ALL_SETTINGS_KEY = 'tr-v-ui-value-set-table-show-all';
/**
* Recursively groups |values|.
* TODO(benjhayden): Use ES6 Maps instead of dictionaries?
*
* @param {!Array.<!tr.v.Value>} values
* @param {!Array.<!function(!tr.v.Value):(string|number)>} groupingCallbacks
* @return {!(Object|tr.v.Value)}
*/
function organizeValues(values, groupingCallbacks, level) {
if (groupingCallbacks.length === level) {
// Recursion base case: there should only be a single value when we've
// grouped by every possible grouping.
if (values.length > 1) {
console.warn('Multiple Values with same name, benchmarkName, ' +
'storyGroupingKeys, storyName, start, storysetRepeatCounter, ' +
'storyRepeatCounter, and displayLabel', values);
}
return values[0];
}
// Group the values by the current grouping.
var groupedValues = tr.b.group(values, groupingCallbacks[level]);
// Skip this grouping level if it contains only a single group,
// but never skip the zeroth grouping level (value name) nor the last
// (displayLabel).
if (level > 0 && level < (groupingCallbacks.length - 1) &&
tr.b.dictionaryLength(groupedValues) === 1) {
return organizeValues(values, groupingCallbacks, level + 1);
}
// Recursively group groupedValues.
return tr.b.mapItems(groupedValues, function(key, groupValues) {
return organizeValues(groupValues, groupingCallbacks, level + 1);
});
}
Polymer({
is: 'tr-v-ui-value-set-table',
/**
* Return true if this view supports this ValueSet.
* Value-set-table supports all possible metrics, so it always returns true.
*
* @param {!tr.v.ValueSet} values
* @return {boolean}
*/
supportsValueSet: function(values) {
return true;
},
/**
* This can optionally depend on the ValueSet.
*
* @return {string}
*/
get tabLabel() {
return 'Table';
},
created: function() {
// TODO(benjhayden): Should these all be ValueSets?
/** @type {undefined|!tr.v.ValueSet} */
this.values_ = undefined;
/** @type {!Array.<!tr.v.Value>} */
this.sourceValues_ = [];
/** @type {!Object} */
this.summaryValuesByGuid_ = {};
this.rows_ = undefined;
this.columns_ = undefined;
},
ready: function() {
this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.CELL;
this.$.table.addEventListener('selection-changed',
this.onSelectionChanged_.bind(this));
this.$.table.addEventListener('selected-column-changed',
this.onSelectedColumnChanged_.bind(this));
this.addEventListener('requestSelectionChange',
this.onRelatedValueSelected_.bind(this));
this.$.search.addEventListener('keyup', this.onSearch_.bind(this));
this.$.show_all.checked = tr.b.Settings.get(SHOW_ALL_SETTINGS_KEY, false);
this.$.show_all.addEventListener('change',
this.onShowAllChange_.bind(this));
},
onShowAllChange_: function() {
tr.b.Settings.set(SHOW_ALL_SETTINGS_KEY, this.$.show_all.checked);
this.updateContents_();
},
onSelectedColumnChanged_: function() {
// Force the table to rebuild the cell values without forgetting which
// rows were expanded.
var expansionStates = this.getExpansionStates_(this.rows_);
this.$.table.tableRows = this.rows_;
this.setExpansionStates_(expansionStates, this.rows_);
},
getExpansionStates_: function(rows) {
var states = {};
for (var i = 0; i < rows.length; ++i) {
var row = rows[i];
if (row.subRows && row.subRows.length &&
this.$.table.getExpandedForTableRow(row)) {
states[i] = this.getExpansionStates_(row.subRows);
}
}
return states;
},
setExpansionStates_: function(states, rows) {
for (var i = 0; i < rows.length; ++i) {
if (states[i]) {
this.$.table.setExpandedForTableRow(rows[i], true);
this.setExpansionStates_(states[i], rows[i].subRows);
}
}
},
onSearch_: function() {
this.updateContents_();
},
rowMatchesSearch_: function(row) {
return row.name.indexOf(this.$.search.value) >= 0;
},
onRelatedValueSelected_: function(event) {
var value = event.selection;
if (!(value instanceof tr.v.Value))
return;
event.stopPropagation();
var displayLabel = getDisplayLabel(value);
var columnIndex = -1;
for (var i = 0; i < this.columns_.length; ++i) {
if (this.columns_[i].title === displayLabel) {
columnIndex = i;
break;
}
}
if (columnIndex < 0)
return;
var hierarchy = [];
var found = false;
function search(row) {
if (row.columns[displayLabel] === value) {
for (var hirow in hierarchy) {
this.$.table.setExpandedForTableRow(hirow, true);
}
found = true;
this.$.table.selectedTableRow = row;
this.$.table.selectedColumnIndex = columnIndex;
return;
}
if (!row.subRows)
return;
hierarchy.push(row);
row.subRows.forEach(search, this);
hierarchy.pop(row);
}
this.rows_.forEach(search, this);
if (found || this.$.show_all.checked)
return;
// Search hidden values for |value|.
for (var test of this.values) {
// Skip values that are already displayed -- we would have found them
// in search() above.
if (this.sourceValues_.indexOf(test) >= 0)
continue;
if (test === value) {
this.$.show_all.checked = true;
this.onShowAllChange_();
this.onRelatedValueSelected_(event);
break;
}
}
},
onSelectionChanged_: function() {
var row = this.$.table.selectedTableRow;
var col = this.$.table.selectedColumnIndex;
var cell = undefined;
if (row && col && this.columns_)
cell = row.columns[this.columns_[col].title];
if ((cell instanceof tr.v.NumericValue) &&
(cell.numeric instanceof tr.v.Numeric)) {
this.$.histogram.style.display = 'block';
this.$.histogram.histogram = cell.numeric;
tr.b.Settings.set(SELECTED_VALUE_SETTINGS_KEY, JSON.stringify({
row: row.name,
column: this.columns_[col].title
}));
} else {
this.$.histogram.style.display = 'none';
}
},
handleFailureValues_: function() {
this.values.map(function(value) {
if (value instanceof tr.v.FailureValue) {
Polymer.dom(this.$.error).textContent = value.description;
this.$.table.style.display = 'none';
this.style.width = '10em';
}
}, this);
},
addDiagnosticSubRows_: function(value, row, column) {
value.diagnostics.forEach(function(name, diagnostic) {
if (name === tr.v.SUMMARY_VALUE_MAP_DIAGNOSTIC_NAME)
return;
// If a previous |value| had a diagnostic with the same name, then
// there is already a subRow that should contain this diagnostic.
for (var subRow of row.subRows) {
if (subRow.name === name) {
subRow.columns[column] = diagnostic;
return;
}
}
// This is the first time that a diagnostic with this name has been
// seen for Values whose name is |value.name|, so create a new subRow.
var subRow = {name: name, columns: {}};
subRow.columns[column] = diagnostic;
row.subRows.push(subRow);
});
},
get values() {
return this.values_;
},
findSummaryValues_: function() {
this.summaryValuesByGuid_ = {};
this.values.map(function(value) {
var summaryValueMap = value.diagnostics.get(
tr.v.SUMMARY_VALUE_MAP_DIAGNOSTIC_NAME);
if (!(summaryValueMap instanceof tr.v.d.RelatedValueMap))
return;
summaryValueMap.values.forEach(function(summaryValue) {
this.summaryValuesByGuid_[summaryValue.guid] = summaryValue;
}, this);
}, this);
},
/**
* @param {!tr.v.ValueSet} values
*/
set values(values) {
this.values_ = values;
this.sourceValues_ = values.sourceValues;
this.findSummaryValues_();
this.updateContents_();
},
updateContents_: function() {
if (!this.values_)
return;
this.style.width = '';
this.$.table.style.display = '';
Polymer.dom(this.$.error).textContent = '';
this.$.histogram.style.display = 'none';
this.handleFailureValues_();
if (Polymer.dom(this.$.error).textContent)
return;
this.buildRows_();
if (this.rows_.length === 0) {
Polymer.dom(this.$.error).textContent = 'zero values';
this.$.table.style.display = 'none';
this.style.width = '10em';
return;
}
this.buildColumns_();
this.$.table.tableColumns = this.columns_;
this.$.table.tableRows = this.rows_;
this.$.table.sortColumnIndex = 0;
this.$.table.rebuild();
this.selectValue_();
tr.b.requestAnimationFrame(function() {
this.style.width = this.$.table.getBoundingClientRect().width;
}, this);
},
selectValue_: function() {
var selectedValue = tr.b.Settings.get(
SELECTED_VALUE_SETTINGS_KEY, undefined);
if (selectedValue) {
selectedValue = JSON.parse(selectedValue);
for (var row of this.rows_) {
if (row.name === selectedValue.row) {
for (var coli = 1; coli < this.columns_.length; ++coli) {
var column = this.columns_[coli];
if (column.title === selectedValue.column) {
this.$.table.selectedTableRow = row;
this.$.table.selectedColumnIndex = coli;
return;
}
}
}
}
}
this.$.table.selectedTableRow = this.rows_[0];
this.$.table.selectedColumnIndex = 1;
},
/**
* Build table rows recursively from organized Values. The recursion stack
* of subRows is maintained in |hierarchy|.
*
* @param {!Object} organizedValues
* @param {!Array.<!Object>} hierarchy
*/
buildRow_: function(organizedValues, hierarchy) {
tr.b.iterItems(organizedValues, function(name, value) {
if (value instanceof tr.v.Value) {
// This recursion base case corresponds to the recursion base case of
// organizeValues(). The last groupingCallback is getDisplayLabel,
// which defines the columns of the table.
// Merge Values up the grouping hierarchy.
for (var row of hierarchy) {
if (row.description === undefined)
row.description = value.description;
if (row.columns[name])
row.columns[name] = row.columns[name].merge(value);
else
row.columns[name] = value;
}
var row = hierarchy[hierarchy.length - 1];
this.addDiagnosticSubRows_(value, row, name);
} else {
// |value| is actually a nested organizedValues.
var row = {name: name, subRows: [], columns: {}};
hierarchy.push(row);
this.buildRow_(value, hierarchy);
hierarchy.pop();
if (hierarchy.length === 0)
this.rows_.push(row);
else
hierarchy[hierarchy.length - 1].subRows.push(row);
}
}, this);
},
get storyGroupingKeys() {
var keys = new Set();
for (var value of this.values) {
var iteration = tr.v.d.IterationInfo.getFromValue(value);
if (!(iteration instanceof tr.v.d.IterationInfo) ||
!iteration.storyGroupingKeys)
continue;
for (var key in iteration.storyGroupingKeys)
keys.add(key);
}
return [...keys.values()].sort();
},
/**
* A ValueSet is a flat set of Values. Value-set-table must present a
* hierarchical view. This method recursively groups this.values as an
* intermediate step towards building tableRows in buildRow_().
* {
* valueA: {
* benchmarkA: {
* storyA: {
* startA: {
* storysetRepeatCounterA: {
* storyRepeatCounterA: {
* displayLabelA: Value,
* displayLabelB: Value
* }
* }
* }
* }
* }
* }
* }
* @return {!Object}
*/
get organizedValues_() {
var showingValues = this.$.show_all.checked ?
this.values : this.sourceValues_;
var values = [];
for (var value of showingValues)
if (this.summaryValuesByGuid_[value.guid] === undefined)
values.push(value);
var groupingCallbacks = [];
groupingCallbacks.push(v => v.name);
groupingCallbacks.push(
v => getIterationInfoField(v, 'benchmarkName', ''));
for (var storyGroupingKey of this.storyGroupingKeys) {
// Javascript closures are dumb.
groupingCallbacks.push((sgk => (
v => getStoryGroupingKeyLabel(v, sgk)))(storyGroupingKey));
}
groupingCallbacks.push(
v => getIterationInfoField(v, 'storyDisplayName', ''));
groupingCallbacks.push(
v => getIterationInfoField(v, 'benchmarkStartString', ''));
groupingCallbacks.push(
v => getIterationInfoField(v, 'storysetRepeatCounterLabel', 0));
groupingCallbacks.push(
v => getIterationInfoField(v, 'storyRepeatCounterLabel', 0));
groupingCallbacks.push(getDisplayLabel);
return organizeValues(values, groupingCallbacks, 0);
},
/* this.rows_ will look something like
* [
* {
* name: 'value name',
* columns: {
* displayLabelA: Value,
* displayLabelB: Value,
* },
* subRows: [
* {
* name: 'benchmark name if multiple',
* columns: {
* displayLabelA: Value,
* displayLabelB: Value,
* },
* subRows: [
* {
* name: 'story name if multiple',
* columns: {
* displayLabelA: Value,
* displayLabelB: Value,
* },
* subRows: [
* {
* name: 'benchmark start if multiple',
* columns: {
* displayLabelA: Value,
* displayLabelB: Value,
* },
* subRows: [
* {
* name: 'storyset repeat counter if multiple',
* columns: {
* displayLabelA: Value,
* displayLabelB: Value,
* },
* subRows: [
* {
* name: 'story repeat counter if multiple',
* columns: {
* displayLabelA: Value,
* displayLabelB: Value,
* },
* subRows: [
* {
* name: 'diagnostic map key',
* columns: {
* displayLabelA: Diagnostic,
* displayLabelB: Diagnostic,
* },
* }
* ]
* }
* ]
* }
* ]
* }
* ]
* }
* ]
* }
* ]
* }
* ]
*
* Any of those layers may be missing except 'value name'.
*/
buildRows_: function() {
this.rows_ = [];
var hierarchy = [];
this.buildRow_(this.organizedValues_, hierarchy);
this.rows_ = this.rows_.filter(this.rowMatchesSearch_.bind(this));
},
get startTimesForDisplayLabels() {
var startTimesForDisplayLabels = {};
for (var value of this.values) {
if (this.summaryValuesByGuid_[value.guid])
continue;
var displayLabel = getDisplayLabel(value);
startTimesForDisplayLabels[displayLabel] = Math.min(
startTimesForDisplayLabels[displayLabel] || 0,
getIterationInfoField(
value, 'benchmarkStart', new Date(0)).getTime());
}
return startTimesForDisplayLabels;
},
get displayLabels() {
var startTimesForDisplayLabels = this.startTimesForDisplayLabels;
var displayLabels = Object.keys(startTimesForDisplayLabels);
displayLabels.sort(function(a, b) {
return startTimesForDisplayLabels[a] - startTimesForDisplayLabels[b];
});
return displayLabels;
},
buildColumn_: function(displayLabel) {
function getValueForValue(value) {
return value.numeric instanceof tr.v.Numeric ? value.numeric.average :
value.numeric.value;
}
return {
title: displayLabel,
align: tr.ui.b.TableFormat.ColumnAlignment.RIGHT,
supportsCellSelection: true,
selectable: true,
value: function(row) {
var cell = row.columns[displayLabel];
if (cell === undefined)
return '';
if (cell instanceof tr.v.NumericValue) {
if (this.$.table.selectedTableColumn &&
this.$.table.selectedTableColumn.title !== displayLabel) {
var referenceCell = row.columns[
this.$.table.selectedTableColumn.title];
if (referenceCell instanceof tr.v.NumericValue &&
cell.numeric.unit === referenceCell.numeric.unit) {
var significance = tr.v.Significance.DONT_CARE;
if (cell.numeric instanceof tr.v.Numeric &&
referenceCell.numeric instanceof tr.v.Numeric) {
significance = cell.numeric.getDifferenceSignificance(
referenceCell.numeric);
}
return tr.v.ui.createScalarSpan(
getValueForValue(cell) - getValueForValue(referenceCell),
{unit: cell.numeric.unit.correspondingDeltaUnit,
significance: significance});
}
}
return tr.v.ui.createScalarSpan(cell);
}
if (cell instanceof tr.v.d.Diagnostic) {
var span = tr.v.ui.createDiagnosticSpan(cell);
span.style.textAlign = 'left';
return span;
}
throw new Error('Invalid cell', cell);
}.bind(this),
cmp: function(rowA, rowB) {
var cellA = rowA.columns[displayLabel];
var cellB = rowB.columns[displayLabel];
if (!(cellA instanceof tr.v.NumericValue) ||
!(cellB instanceof tr.v.NumericValue)) {
return undefined;
}
var valueA = getValueForValue(cellA);
var valueB = getValueForValue(cellB);
// If a reference column is selected, compare the *differences*
// between the two cells and their references.
if (this.$.table.selectedTableColumn &&
this.$.table.selectedTableColumn.title !== displayLabel) {
var referenceColumn = this.$.table.selectedTableColumn.title;
var referenceCellA = rowA.columns[referenceColumn];
var referenceCellB = rowB.columns[referenceColumn];
if (referenceCellA instanceof tr.v.NumericValue &&
referenceCellB instanceof tr.v.NumericValue &&
cellA.numeric.unit === referenceCellA.numeric.unit &&
cellB.numeric.unit === referenceCellB.numeric.unit) {
valueA -= getValueForValue(referenceCellA);
valueB -= getValueForValue(referenceCellB);
}
}
return valueA - valueB;
}.bind(this)
};
},
buildColumns_: function() {
this.columns_ = [
{
title: 'Name',
align: tr.ui.b.TableFormat.ColumnAlignment.LEFT,
supportsCellSelection: false,
value: function(row) {
var nameEl = document.createElement('span');
Polymer.dom(nameEl).textContent = row.name;
if (row.description)
nameEl.title = row.description;
nameEl.style.textOverflow = 'ellipsis';
return nameEl;
},
cmp: (a, b) => a.name.localeCompare(b.name)
}
];
for (var displayLabel of this.displayLabels)
this.columns_.push(this.buildColumn_(displayLabel));
}
});
tr.v.ui.registerValueSetView('tr-v-ui-value-set-table');
return {};
});
</script>