blob: b8cf89cc241fe68217453959625004a2ff669c50 [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/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] = Array.from(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>