blob: f213ff057d6144142c4c8b879373735f7bb6a167 [file] [log] [blame]
<!-- The <explore-sk> custom element declaration.
Main page of Perf, for exploring data.
Attributes:
None.
Events:
None.
Methods:
None.
-->
<link rel="import" href="/res/imp/bower_components/iron-flex-layout/iron-flex-layout-classes.html">
<link rel="import" href="/res/imp/bower_components/iron-icon/iron-icon.html">
<link rel="import" href="/res/imp/bower_components/iron-icons/iron-icons.html">
<link rel="import" href="/res/imp/bower_components/paper-input/paper-input.html">
<link rel="import" href="/res/imp/bower_components/paper-spinner/paper-spinner.html">
<link rel="import" href="/res/imp/bower_components/paper-tabs/paper-tabs.html">
<link rel="import" href="/res/imp/bower_components/paper-checkbox/paper-checkbox.html">
<link rel="import" href="/res/common/imp/query2-sk.html" />
<link rel="import" href="/res/common/imp/paramset.html" />
<link rel="import" href="/res/common/imp/query-summary-sk.html" />
<link rel="stylesheet" href="/res/common/css/md.css">
<link rel="import" href="/res/imp/plot-simple.html" />
<link rel="import" href="/res/imp/commit-detail-panel.html" />
<link rel="import" href="/res/imp/highlight.html" />
<link rel="import" href="/res/imp/day-range.html" />
<link rel="import" href="/res/imp/json-source.html" />
<dom-module id="explore-sk">
<style include="iron-flex iron-flex-alignment iron-positioning">
h3 {
margin: 0.5em 0 0 0;
}
paper-input-decorator {
margin: 0.5em 0;
}
#selections {
margin: 0 1em;
}
#commits,
#paramset {
display: inline-block;
margin: 0.5em;
overflow-y: auto;
}
#paramset {
height: 480px;
}
#details.hidden,
#queryTab.hidden,
#commits.hidden,
#paramset.hidden,
#simple_paramset.hidden {
display: none;
}
paper-checkbox {
--paper-checkbox-checked-color: #1f78b4;
--paper-checkbox-checked-ink-color: #1f78b4;
--paper-checkbox-unchecked-color: #1f78b4;
--paper-checkbox-unchecked-ink-color: #1f78b4;
--paper-checkbox-label-color: #1f78b4;
margin: 0.6em;
}
paper-tabs {
background: #A6CEE3;
color: #555;
}
paper-tab.iron-selected {
background: #1F78B4;
color: white;
border: #1F78B4 solid 2px;
}
paper-tab {
border: white solid 2px;
}
#tabs {
margin: 1em 0 0 1em;
}
#detail {
border: #1F78B4 2px solid;
margin: 0;
}
#queryTab {
margin: 1em;
}
#buttons {
margin: 1em;
}
iron-icon {
color: #1F78B4;
}
day-range-sk {
display: block;
}
#commits {
margin: 1em;
}
#simple_paramset {
margin: 1em 0 1em 1em;
}
#percent {
margin: 0.6em;
font-family: 'Roboto Mono',monospace;
width: 3em;
}
</style>
<template>
<div>
<div class="layout vertical">
<div class="layout horizontal">
<div id=plotDiv class="layout flex">
<highlightbar-sk id=highlight></highlightbar-sk>
<plot-simple-sk width=1024 height=256 id=plot></plot-simple-sk>
<div class="layout horizontal center-justified">
<day-range-sk id=range on-day-range-change="_rangeChange"></day-range-sk>
<paper-spinner id=spinner></paper-spinner>
<span id=percent></span>
</div>
</div>
<div class="layout vertical" id=buttons>
<button on-click="_removeHighlighted" title="Remove all the highlighted traces.">Remove Highlighted</button>
<button on-click="_removeAll" title="Remove all the traces.">Remove All</button>
<button on-click="_highlightedOnly" title="Remove all but the highlighted traces.">Highlighted Only</button>
<button on-click="_clearHighlights" title="Remove highlights from all traces.">Clear Highlights</button>
<button on-click="_resetAxes" title="Reset back to the original zoom level.">Reset Axes</button>
<button on-click="_shiftBoth" title="Expand the display [[_numShift]] commits in
both directions.">&lt;&lt; +[[_numShift]] &gt;&gt;</button>
<button on-click="_shiftLeft" title="Move 10 commits in the past.">&lt;&lt; [[_numShift]]</button>
<button on-click="_shiftRight" title="Move 10 commits in the future.">[[_numShift]] &gt;&gt;</button>
<div>
<button on-click="_zoomToRange" id=zoom_range disabled title="Fit the time range to the current zoom window.">Zoom Range</button>
<span title="Number of commits skipped between each point displayed." hidden="[[_isZero(_dataframe.skip)]]" id=skip>[[_dataframe.skip]]</span>
</div>
<paper-checkbox checked="{{_show_zero}}" title="Toggle the presence of the zero line.">Zero</paper-checkbox>
</div>
</div>
<div id=tabs class="flex">
<paper-tabs selected=0 id=detailtab no-bar on-iron-select="_tabSelect">
<paper-tab>Query</paper-tab>
<paper-tab>Params</paper-tab>
<paper-tab id=commitsTab disabled>Details</paper-tab>
</paper-tabs>
<div id=detail>
<div id=queryTab class="layout vertical">
<div class="layout horizontal">
<query2-sk id=query></query2-sk>
<div class="layout vertical" id=selections>
<h3>Selections</h3>
<query-summary-sk id=summary></query-summary-sk>
<div>
Matches: <span id=matches></span>
</div>
<button on-tap="_add" class=action>Plot</button>
</div>
</div>
<h3>Calculated Traces</h3>
<div class="layout horizontal center">
<paper-input-decorator floatingLabel label="Formula" flex>
<textarea id="formula" rows=3 cols=80></textarea>
</paper-input-decorator>
<button on-tap="_addCalculated" class=action>Add</button>
<a href="/help/" target=_blank><iron-icon icon="help"></iron-icon></a>
</div>
</div>
<paramset-sk id=paramset class=hidden clickable-values></paramset-sk>
<div id=details class="layout horizontal wrap hidden">
<paramset-sk id=simple_paramset clickable-values></paramset-sk>
<div class="layout vertical">
<commit-detail-panel-sk id=commits></commit-detail-panel-sk>
<json-source id=jsonsource></json-source>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</dom-module>
<script>
Polymer({
is: "explore-sk",
properties: {
// Keep track of the data sent to plot.
_lines: {
type: Object,
value: function() { return {}; },
},
_dataframe: {
type: Object,
value: function() { return {
traceset: {},
}; },
},
// Keep track of whether a request is inflight to count the number of traces that match the current query.
_countInProgress: {
type: Boolean,
value: false,
},
// The state that goes into the URL.
//
state: {
type: Object,
value: function() { return {
begin: Math.floor(Date.now()/1000 - 24*60*60),
end: Math.floor(Date.now()/1000),
formulas: [],
queries: [],
keys: "", // The id of the shortcut to a list of trace keys.
xbaroffset: -1, // The offset of the commit in the repo.
}; },
},
// The [begin, end] timestamps of the region
// that the user is zoomed in on.
_zoomRange: {
type: Array,
value: function() { return []; },
},
// The id of the current frame request. Will be the empty string
// if there is no pending request.
_requestId: {
type: String,
value: "",
},
_show_zero: {
type: Boolean,
value: true,
observer: "_zeroChanged",
},
_numShift: {
type: Number,
value: sk.perf.num_shift,
},
},
ready: function() {
this.ZERO_NAME = "special_zero";
// Populate the query element.
var tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
sk.get("/_/initpage/?tz=" + tz).then(JSON.parse).then(function(json) {
var header = json.dataframe.header;
this.state.begin = this.$.range.begin = header[0].timestamp;
this.state.end = this.$.range.end = header[header.length-1].timestamp+1;
this.$.query.setKeyOrder(sk.perf.key_order);
this.$.query.setParamset(json.dataframe.paramset);
// Remove the paramset so it doesn't get displayed in the Params tab.
json.dataframe.paramset = {};
// Display this SearchResult, which has no traces, but this will
// set up the tick marks, the skp bands, and the 0-trace.
this._addTraces(json, true, false);
// From this point on reflect the state to the URL.
this._startStateReflector();
}.bind(this)).catch(sk.errorMessage);
// Reflect the current query to the query summary.
this.$.query.addEventListener("query-change", function(e){
var query = e.detail.q;
this.$.summary.selection = query;
this._updateCount();
var formula = this.$.formula.value;
if (formula == "") {
this.$.formula.value = 'filter("' + query + '")';
} else if (2 == (formula.match(/\"/g) || []).length) {
// Only update the filter query if there's one string in the formula.
this.$.formula.value = formula.replace(/".*"/, '"' + query + '"');
}
}.bind(this));
// Highlight trace when a paramset value is clicked.
this.$.paramset.addEventListener("paramset-key-value-click",
this._paramsetKeyValueClick.bind(this));
this.$.simple_paramset.addEventListener("paramset-key-value-click",
this._paramsetKeyValueClick.bind(this));
// Reflect the focused trace in the paramset.
this.$.plot.addEventListener("trace_focused", function(e) {
this.$.paramset.clearHighlight();
this.$.paramset.setHighlight(sk.key.toObject(e.detail.id));
this.$.highlight.key = e.detail.id;
this.$.highlight.value = e.detail.pt[1];
}.bind(this));
// User has zoomed in on the graph.
this.$.plot.addEventListener("zoom", function(e) {
var begin = this._dataframe.header[Math.floor(e.detail.xMin)].timestamp;
var endIndex = Math.min(Math.floor(e.detail.xMax), this._dataframe.header.length-1);
var end = this._dataframe.header[endIndex].timestamp+1;
this._zoomRange = [begin, end];
this.$.zoom_range.disabled = false;
}.bind(this));
// Highlight a trace when it is clicked on.
this.$.plot.addEventListener("trace_selected", function(e) {
this.$.plot.clearHighlight();
this.$.plot.setHighlight(e.detail.id);
this.$.commits.setCommitDetail([]);
var x = +e.detail.pt[0]|0;
// loop backwards from x until you get the next
// non 1e32 point.
var commits = [this._dataframe.header[x]];
var trace = this._dataframe.traceset[e.detail.id];
for (var i = x-1; i >= 0; i--) {
if (trace[i] != 1e32) {
break;
}
commits.push(this._dataframe.header[i]);
}
// Convert the trace id into a paramset to display.
var params = sk.key.toObject(e.detail.id);
var paramset = {}
Object.keys(params).forEach(function(key) {
paramset[key] = [params[key]];
});
// Request populated commits from the server.
sk.post("/_/cid/", JSON.stringify(commits)).then(JSON.parse).then(function(json){
this.$.commits.setCommitDetail(json);
this.$.commitsTab.disabled = false;
this.$.simple_paramset.setParamSets([paramset]);
this.$.detailtab.selected = 2;
this.$.jsonsource.cid = commits[0];
this.$.jsonsource.traceid = e.detail.id;
}.bind(this)).catch(sk.errorMessage);
}.bind(this));
this._updateCount();
this._resizeWatcher();
},
_startStateReflector: function() {
sk.stateReflector(this, function() {
this.$.range.begin = this.state.begin;
this.$.range.end = this.state.end;
this._rangeChangeImpl(this.state.begin, this.state.end);
}.bind(this));
},
_paramsetKeyValueClick: function(e) {
var keys = [];
Object.keys(this._lines).forEach(function(key) {
if (sk.key.matches(key, e.detail.key, e.detail.value)) {
keys.push(key);
}
});
// Additively highlight if the ctrl key is pressed.
if (!e.detail.ctrl) {
this.$.plot.clearHighlight();
}
this.$.plot.setHighlight(keys);
},
// Fit the time range to the zoom being displayed.
// Reload all the queries/formulas on the new time range.
_zoomToRange: function() {
this.$.range.begin = this._zoomRange[0];
this.$.range.end = this._zoomRange[1];
this._rangeChangeImpl(this._zoomRange[0], this._zoomRange[1]);
},
// Called when the day-range slider has changed, causes all the
// queries/formulas to be reloaded for the new time range.
_rangeChange: function(e) {
this._rangeChangeImpl(e.detail.begin, e.detail.end);
},
_shiftBoth: function(e) {
this._shiftImpl(-this._numShift, this._numShift);
},
_shiftRight: function(e) {
this._shiftImpl(this._numShift, this._numShift);
},
_shiftLeft: function(e) {
this._shiftImpl(-this._numShift, -this._numShift);
},
// Change the current range by the following +/- offsets.
_shiftImpl: function(beginOffset, endOffset) {
var body = {
begin: this.state.begin,
begin_offset: beginOffset,
end: this.state.end,
end_offset: endOffset,
}
sk.post('/_/shift/', JSON.stringify(body)).then(JSON.parse).then(function(json) {
this._rangeChangeImpl(json.begin, json.end);
}.bind(this)).catch(sk.errorMessage);
},
// Reload all the queries/formulas on the given time range.
_rangeChangeImpl: function(begin, end) {
this.state.begin = begin;
this.state.end = end;
if (this.state.formulas.length == 0 && this.state.queries.length == 0 && this.state.keys == "") {
return
}
var body = {
begin: this.state.begin,
end: this.state.end,
formulas: this.state.formulas,
queries: this.state.queries,
keys: this.state.keys,
};
var switchToTab = body.formulas.length > 0 || body.queries.length > 0 || body.keys != "";
this._requestFrame(body, function(json) {
this.$.plot.removeAll();
this._lines = [];
this._addTraces(json, false, switchToTab);
this.$.zoom_range.disabled = true;
}.bind(this));
},
_updateCount: function() {
if (this._countInProgress === true) {
return
}
this._countInProgress = true;
sk.post("/_/count/", this.$.query.current_query, "application/x-www-form-urlencoded").then(JSON.parse).then(function(json) {
this._countInProgress = false;
this.$.matches.textContent = json.count;
}.bind(this)).catch(function() {
this._countInProgress = false;
});
},
_zeroChanged: function() {
if (!this._dataframe.header) {
return;
}
if (this._show_zero) {
var line = [];
for (var i = 0; i < this._dataframe.header.length; i++) {
line.push([i, 0]);
}
var lines = {};
lines[this.ZERO_NAME] = line;
this.$.plot.addLines(lines);
} else {
this.$.plot.deleteLine(this.ZERO_NAME);
}
this.$.plot.resetAxes();
},
// _addTraces adds the traces to the graph in the serialized SearchResults
// found in json. If incremental is true then the traces are added w/o
// removing the old traces. If tab is true then the details tab is
// switched to the params for the displayed traces.
_addTraces: function(json, incremental, tab) {
var dataframe = json.dataframe;
var lines = {};
// Add in the 0-trace.
if (this._show_zero) {
dataframe.traceset[this.ZERO_NAME] = Array(dataframe.header.length).fill(0);
}
// Convert the dataframe into a form suitable for the plot element.
var keys = Object.keys(dataframe.traceset);
keys.forEach(function(key) {
var values = [];
dataframe.traceset[key].forEach(function(y, x) {
if (y != 1e32) {
values.push([x, y]);
}
});
lines[key] = values;
});
if (incremental) {
Object.keys(lines).forEach(function(key) {
this._lines[key] = lines[key];
}.bind(this));
if (this._dataframe.header == undefined) {
this._dataframe = dataframe;
} else {
Object.keys(dataframe.traceset).forEach(function(key) {
this._dataframe.traceset[key] = dataframe.traceset[key];
}.bind(this));
}
} else {
this._lines = lines;
this._dataframe = dataframe;
}
if (!incremental) {
this.$.plot.removeAll();
}
this.$.plot.addLines(this._lines);
// Ticks are serialized as a slice of slices, ala [[0, "foo"], [1.5, "bar"], ..]
// because the structure we actually need, a map[float64]string, isn't able
// to be serialized as JSON, so we transform the representations here.
//
// TODO(jcgregorio) Once we are the only customer of plot-simple-sk, then change
// signature of setTicks().
var tickmap = {};
json.ticks.forEach(function(t) {
tickmap[t[0]] = t[1];
});
this.$.plot.setTicks(tickmap);
// Always add in the last index so we draw a band if there's an odd
// number of skp updates.
json.skps.push(json.dataframe.header.length-1);
var bands = [];
for (var i = 1; i < json.skps.length; i+=2) {
bands.push([json.skps[i-1], json.skps[i]]);
}
this.$.plot.setBanding(bands);
// Populate the xbar if present.
if (this.state.xbaroffset != -1) {
var xbaroffset = this.state.xbaroffset;
var xbar = -1;
this._dataframe.header.forEach(function(h, i) {
if (h.offset == xbaroffset) {
xbar = i;
}
});
if (xbar != -1) {
this.$.plot.setXBar(xbar);
} else {
this.$.plot.clearXBar();
}
} else {
this.$.plot.clearXBar();
}
// Populate the paramset element.
this.$.paramset.setParamSets([dataframe.paramset]);
if (tab) {
this.$.detailtab.selected = 1;
}
},
_add: function() {
var q = this.$.query.current_query.trim();
if (q == "") {
return
}
if (this.state.queries.indexOf(q) == -1) {
this.state.queries.push(q);
}
var body = {
begin: this.state.begin,
end: this.state.end,
queries: [q],
};
this._requestFrame(body, function(json) {
this._addTraces(json, true, true);
}.bind(this));
},
_tabSelect: function() {
var sel = this.$.detailtab.selected;
this.$.queryTab.classList.toggle('hidden', !(sel == 0));
this.$.paramset.classList.toggle('hidden', !(sel == 1));
this.$.details.classList.toggle('hidden', !(sel == 2));
},
// Watch the size of the plot parents element and when it changes then
// then resize the plot element to match.
_resizeWatcher: function () {
this.async(this._resizeWatcher.bind(this), 300);
var w = Math.round(window.getComputedStyle(this.$.plotDiv, null).getPropertyValue('width').slice(0, -2));
if (w != this.$.plot.width) {
this.$.plot.setAttribute('width', w);
// Measure again since the resize of svg might have caused a tiny change in the size of
// the parent window.
w = Math.round(window.getComputedStyle(this.$.plotDiv, null).getPropertyValue('width').slice(0, -2));
this.$.plot.setAttribute('width', w);
this.$.plot.resetAxes();
}
},
_removeAll: function() {
this.state.formulas = [];
this.state.queries = [];
this.state.keys = "";
this.$.plot.removeAll();
this._lines = [];
},
// When Remove Highlighted or Highlighted Only are pressed then create a
// shortcut for just the traces that are displayed.
//
// Note that removing a trace doesn't work if the trace came from a
// formula that returns multiple traces. This is a known issue that
// isn't currently worth solving.
_reShortCut: function(keys) {
if (keys.length == 0) {
this.state.keys = "";
this.state.queries = [];
return
}
var state = {
keys: keys,
};
sk.post('/_/keys/', JSON.stringify(state)).then(JSON.parse).then(function (json) {
this.state.keys = json.id;
this.state.queries = [];
}.bind(this));
},
_removeHighlighted: function() {
var ids = this.$.plot.highlighted();
var toadd = {};
var toShortcut = [];
Object.keys(this._dataframe.traceset).forEach(function(key) {
if (ids.indexOf(key) != -1) {
// Detect if it is a formula being removed.
if (this.state.formulas.indexOf(key) != -1) {
this.state.formulas.splice(this.state.formulas.indexOf(key), 1)
}
return
}
if (key[0] == ",") {
toShortcut.push(key);
}
var values = [];
this._dataframe.traceset[key].forEach(function(y, x) {
if (y != 1e32) {
values.push([x, y]);
}
});
toadd[key] = values;
}.bind(this));
// Remove the traces from the traceset so they don't reappear.
ids.forEach(function(key) {
if (this._dataframe.traceset[key] != undefined) {
delete this._dataframe.traceset[key];
}
}.bind(this));
this._lines = toadd;
this.$.plot.removeAll();
this.$.plot.addLines(toadd);
this._reShortCut(toShortcut);
},
_highlightedOnly: function() {
var ids = this.$.plot.highlighted();
var toadd = {};
var toremove = [];
var toShortcut = [];
Object.keys(this._dataframe.traceset).forEach(function(key) {
if (ids.indexOf(key) == -1 && !key.startsWith("special")) {
// Detect if it is a formula being removed.
if (this.state.formulas.indexOf(key) != -1) {
this.state.formulas.splice(this.state.formulas.indexOf(key), 1)
} else {
toremove.push(key);
}
return
}
if (key[0] == ",") {
toShortcut.push(key);
}
var values = [];
this._dataframe.traceset[key].forEach(function(y, x) {
if (y != 1e32) {
values.push([x, y]);
}
});
toadd[key] = values;
}.bind(this));
// Remove the traces from the traceset so they don't reappear.
Object.keys(this._dataframe.traceset).forEach(function(key) {
if (key in toremove) {
delete this._dataframe.traceset[key];
}
}.bind(this));
this._lines = toadd;
this.$.plot.removeAll();
this.$.plot.addLines(toadd);
this._reShortCut(toShortcut);
},
_clearHighlights: function() {
this.$.plot.clearHighlight();
},
_resetAxes: function() {
this.$.plot.resetAxes();
this.$.zoom_range.disabled = true;
},
_addCalculated: function() {
var f = this.$.formula.value;
if (f.trim() == "") {
return
}
if (this.state.formulas.indexOf(f) == -1) {
this.state.formulas.push(f);
}
var body = {
begin: this.state.begin,
end: this.state.end,
formulas: [f],
};
this._requestFrame(body, function(json) {
// TODO(jcgregorio) Remove all returned trace ids from hidden.
this._addTraces(json, true, false);
}.bind(this));
},
// Common catch function for _requestFrame and _checkFrameRequestStatus.
_catch: function(msg) {
this._requestId = "";
if (msg) {
sk.errorMessage(msg, 10000);
}
this.$.percent.textContent = "";
this.$.spinner.active = false;
},
// Requests a new dataframe, where body is a serialized FrameRequest:
//
// {
// begin: 1448325780,
// end: 1476706336,
// formulas: ["ave(filter("name=desk_nytimes.skp&sub_result=min_ms"))"],
// hidden: [],
// queries: [
// "name=AndroidCodec_01_original.jpg_SampleSize8",
// "name=AndroidCodec_1.bmp_SampleSize8"],
// tz: "America/New_York"
// };
//
// The 'cb' callback function will be called with the decoded JSON body
// of the response once it's available.
_requestFrame: function(body, cb) {
body.tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (this._requestId != "") {
sk.errorMessage("There is a pending query already running.");
return
}
this.$.spinner.active = true;
sk.post("/_/frame/start", JSON.stringify(body), "application/json").then(JSON.parse).then(function(json) {
this._requestId = json.id;
this._checkFrameRequestStatus(cb);
}.bind(this)).catch(this._catch.bind(this));
},
// Periodically check the status of a pending FrameRequest, calling the
// 'cb' callback with the decoded JSON upon success.
_checkFrameRequestStatus: function(cb) {
sk.get("/_/frame/status/"+this._requestId).then(JSON.parse).then(function(json) {
if (json.state == "Running") {
this.$.percent.textContent = Math.floor(json.percent*100) + "%";
window.setTimeout(this._checkFrameRequestStatus.bind(this, cb), 300);
} else {
sk.get("/_/frame/results/"+this._requestId).then(JSON.parse).then(function(json) {
cb(json);
this._catch(json.msg);
}.bind(this)).catch(this._catch.bind(this));
}
}.bind(this)).catch(this._catch.bind(this));
},
_isZero: function(n) {
return n === 0;
}
});
</script>