| <!-- 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="stylesheet" href="/res/common/css/md.css"> |
| |
| <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; |
| } |
| |
| #detail { |
| border: #1F78B4 2px solid; |
| margin: 0; |
| } |
| |
| #queryTab { |
| margin: 1em; |
| } |
| |
| iron-icon { |
| color: #1F78B4; |
| } |
| |
| domain-picker-sk { |
| display: inline-block; |
| } |
| |
| #commits { |
| margin: 1em; |
| } |
| |
| #simple_paramset { |
| margin: 1em 0 1em 1em; |
| } |
| |
| #percent { |
| margin: 0.6em; |
| font-family: 'Roboto Mono',monospace; |
| } |
| |
| #plot { |
| grid-area: graph; |
| display: block; |
| min-height: 30vh; |
| } |
| |
| #buttons { |
| grid-area: buttons; |
| font-size: 11px; |
| align-self: center; |
| } |
| |
| #tabs { |
| grid-area: tabs; |
| } |
| |
| :host { |
| width: 100%; |
| display: inline-grid; |
| grid-template-columns: auto; |
| grid-template-rows: auto auto auto; |
| grid-template-areas: |
| "buttons" |
| "graph" |
| "tabs"; |
| } |
| |
| #spinner[aria-hidden=true] { |
| display: none; |
| } |
| |
| #spin-container { |
| display: inline-block; |
| } |
| |
| #trace_id { |
| display: block; |
| padding: 1em; |
| font-size: 12pt; |
| } |
| |
| json-source-sk { |
| padding: 1em; |
| } |
| |
| </style> |
| <template> |
| <plot-simple-sk width=1024 height=256 id=plot></plot-simple-sk> |
| |
| <div id=buttons> |
| <domain-picker-sk id=range state="[[state]]" on-domain-changed="_rangeChange"></domain-picker-sk> |
| <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="_shiftLeft" title="Move 10 commits in the past."><< [[_numShift]]</button> |
| <button on-click="_shiftBoth" title="Expand the display [[_numShift]] commits in |
| both directions."><< +[[_numShift]] >></button> |
| <button on-click="_shiftRight" title="Move 10 commits in the future.">[[_numShift]] >></button> |
| <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> |
| <paper-checkbox on-click=_zeroChanged checked="{{state.show_zero}}" title="Toggle the presence of the zero line.">Zero</paper-checkbox> |
| <paper-checkbox on-click=_autoRefreshChanged checked="{{state.auto_refresh}}" title="Auto-refresh the data displayed in the graph.">Auto-Refresh</paper-checkbox> |
| <button on-click="_normalize" title="Apply norm() to all the traces.">Normalize</button> |
| <button on-click="_scale_by_ave" title="Apply scale_by_ave() to all the traces.">Scale By Ave</button> |
| <button on-click="_csv" title="Download all displayed data as a CSV file.">CSV</button> |
| <a href="" target=_blank download="traces.csv" id=csv_download></a> |
| <div id=spin-container> |
| <paper-spinner id=spinner></paper-spinner> |
| <span id=percent></span> |
| </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> |
| <div> |
| <paper-input floatingLabel label="Trace ID" readonly id=trace_id></paper-input> |
| <paramset-sk id=paramset class=hidden clickable-values></paramset-sk> |
| </div> |
| <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-sk id=jsonsource></json-source-sk> |
| </div> |
| </div> |
| </div> |
| </div> |
| </template> |
| </dom-module> |
| |
| <script> |
| (function () { |
| // MISSING_DATA_SENTINEL signifies a missing sample value. |
| // |
| // JSON doesn't support NaN or +/- Inf, so we need a valid float32 to signal |
| // missing data that also has a compact JSON representation. |
| // |
| // The mirror Go definition is in infra/go/vec32. |
| const MISSING_DATA_SENTINEL = 1e32; |
| |
| 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. |
| show_zero: true, |
| auto_refresh: false, |
| num_commits: 50, |
| request_type: 1, |
| }; }, |
| }, |
| // 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, |
| }, |
| _refreshId: { |
| type: Number, |
| value: -1, |
| }, |
| _csvBlob: { |
| type: Object, |
| value: null, |
| } |
| }, |
| |
| |
| ready: function() { |
| this.ZERO_NAME = "special_zero"; |
| |
| this.REFRESH_TIMEOUT = 30*1000; // milliseconds |
| |
| // Populate the query element. |
| var tz = Intl.DateTimeFormat().resolvedOptions().timeZone; |
| sk.get("/_/initpage/?tz=" + tz).then(JSON.parse).then(function(json) { |
| let now = Math.floor(Date.now()/1000); |
| this.state.begin = now - 60*60*24; |
| this.state.end = now; |
| this.$.range.state = this.state; |
| |
| 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 = {}; |
| |
| // 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; |
| 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)); |
| |
| this.$.query.addEventListener("query-change-delayed", function(e){ |
| this._updateCount(); |
| }.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.$.trace_id.value = e.detail.id; |
| }.bind(this)); |
| |
| // User has zoomed in on the graph. |
| this.$.plot.addEventListener("zoom", function(e) { |
| this._zoomRange = [e.detail.xMin/1000, e.detail.xMax/1000]; |
| 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.details = []; |
| |
| var x = +e.detail.pt[0]|0; |
| // loop backwards from x until you get the next |
| // non MISSING_DATA_SENTINEL 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] != MISSING_DATA_SENTINEL) { |
| 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.details = 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(); |
| }, |
| |
| _startStateReflector: function() { |
| sk.stateReflector(this, function() { |
| this.$.range.state = this.state; |
| this._zeroChanged(); |
| this._autoRefreshChanged(); |
| this._rangeChangeImpl(); |
| }.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.state.begin = this._zoomRange[0]; |
| this.state.end = this._zoomRange[1]; |
| this._rangeChangeImpl(); |
| }, |
| |
| // Called when the domain-picker-sk control has changed, causes all the |
| // queries/formulas to be reloaded for the new time range. |
| _rangeChange: function(e) { |
| this.state.begin = e.detail.state.begin; |
| this.state.end = e.detail.state.end; |
| this.state.num_commits = e.detail.state.num_commits; |
| this.state.request_type = e.detail.state.request_type; |
| this._rangeChangeImpl(); |
| }, |
| |
| _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, |
| num_commits: this.state.num_commits, |
| request_type: this.state.request_type, |
| } |
| sk.post('/_/shift/', JSON.stringify(body)).then(JSON.parse).then(function(json) { |
| this.state.begin = json.begin; |
| this.state.end = json.end; |
| this.state.num_commits = json.num_commits; |
| // Now let's try to fool Polymer into updating the page. |
| this.set('state', Object.assign({}, this.state)); |
| this._rangeChangeImpl(); |
| }.bind(this)).catch(sk.errorMessage); |
| }, |
| |
| // Fill in the basic data needed for a FrameRequest that will be common |
| // to all situations. |
| _requestFrameBodyFromState: function() { |
| return { |
| begin: this.state.begin, |
| end: this.state.end, |
| num_commits: this.state.num_commits, |
| request_type: this.state.request_type, |
| }; |
| }, |
| |
| // Create a FrameRequest that will re-create the current state of the page. |
| _requestFrameBodyFullFromState: function() { |
| var body = this._requestFrameBodyFromState(); |
| return Object.assign(body, { |
| formulas: this.state.formulas, |
| queries: this.state.queries, |
| keys: this.state.keys, |
| }); |
| }, |
| |
| // Reload all the queries/formulas on the given time range. |
| _rangeChangeImpl: function(begin, end) { |
| if (!this.state) { |
| return |
| } |
| if (this.state.formulas.length == 0 && this.state.queries.length == 0 && this.state.keys == "") { |
| return |
| } |
| |
| if (this.$.trace_id) { |
| this.$.trace_id.value = ''; |
| } |
| var body = this._requestFrameBodyFullFromState(); |
| var switchToTab = body.formulas.length > 0 || body.queries.length > 0 || body.keys != ""; |
| this._requestFrame(body, function(json) { |
| if (json == null) { |
| sk.errorMessage('Failed to find any matching traces.'); |
| return |
| } |
| this.$.plot.removeAll(); |
| this._lines = []; |
| this._addTraces(json, false, switchToTab); |
| this.$.plot.resetAxes(); |
| this.$.zoom_range.disabled = true; |
| }.bind(this)); |
| }, |
| |
| _updateCount: function() { |
| if (this._countInProgress === true) { |
| return |
| } |
| this._countInProgress = true; |
| let body = { |
| q: this.$.query.current_query, |
| begin: this.state.begin, |
| end: this.state.end, |
| }; |
| sk.post("/_/count/", JSON.stringify(body)).then(JSON.parse).then(function(json) { |
| this._countInProgress = false; |
| this.$.matches.textContent = json.count; |
| |
| if (json.paramset) { |
| this.$.query.setParamset(json.paramset); |
| } |
| }.bind(this)).catch(function() { |
| this._countInProgress = false; |
| }); |
| }, |
| |
| _zeroChanged: function() { |
| if (!this._dataframe.header) { |
| return; |
| } |
| if (this.state.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(); |
| }, |
| |
| _autoRefreshChanged: function() { |
| if (!this.state.auto_refresh) { |
| if (this._refreshId != -1) { |
| clearInterval(this._refreshId); |
| } |
| } else { |
| this._refreshId = setInterval(() => this._autoRefresh(), this.REFRESH_TIMEOUT); |
| } |
| }, |
| |
| _autoRefresh: function() { |
| // Update end to be now. |
| this.state.end = Math.floor(Date.now()/1000); |
| var body = this._requestFrameBodyFullFromState(); |
| var switchToTab = body.formulas.length > 0 || body.queries.length > 0 || body.keys != ""; |
| this._requestFrame(body, (json) => { |
| this.$.plot.removeAll(); |
| this._lines = []; |
| this._addTraces(json, false, switchToTab); |
| this.$.zoom_range.disabled = true; |
| }); |
| }, |
| |
| // _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 = {}; |
| |
| if (dataframe.traceset === null) { |
| return |
| } |
| |
| // Add in the 0-trace. |
| if (this.state.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 != MISSING_DATA_SENTINEL) { |
| values.push([x, y]); |
| } else { |
| values.push([x, null]); |
| } |
| }); |
| 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(); |
| } |
| var labels = []; |
| dataframe.header.forEach(header => { |
| labels.push(new Date(header.timestamp * 1000)); |
| }); |
| |
| this.$.plot.addLines(this._lines, labels); |
| |
| // 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 = this._requestFrameBodyFromState(); |
| Object.assign(body, { |
| 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 = window.innerWidth * 0.75; |
| 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 = window.innerWidth * 0.75; |
| 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. |
| // |
| // Returns the Promise that's creating the shortcut, or undefined if |
| // there isn't a shortcut to create. |
| _reShortCut: function(keys) { |
| if (keys.length == 0) { |
| this.state.keys = ""; |
| this.state.queries = []; |
| return undefined |
| } |
| var state = { |
| keys: keys, |
| }; |
| return sk.post('/_/keys/', JSON.stringify(state)).then(JSON.parse).then(function (json) { |
| this.state.keys = json.id; |
| this.state.queries = []; |
| }.bind(this)); |
| }, |
| |
| // Create a shortcut for all of the traces currently being displayed. |
| // |
| // Returns the Promise that's creating the shortcut, or undefined if |
| // there isn't a shortcut to create. |
| _shortcutAll: function() { |
| var toShortcut = []; |
| |
| Object.keys(this._dataframe.traceset).forEach(function(key) { |
| if (key[0] == ",") { |
| toShortcut.push(key); |
| } |
| }); |
| |
| return this._reShortCut(toShortcut); |
| }, |
| |
| // Apply norm() to all the traces currently being displayed. |
| _normalize: function() { |
| let promise = this._shortcutAll(); |
| if (!promise) { |
| sk.errorMessage("No traces to normalize."); |
| return |
| } |
| promise.then(() => { |
| let f = `norm(shortcut("${this.state.keys}"))` |
| this._removeAll(); |
| var body = this._requestFrameBodyFromState(); |
| Object.assign(body, { |
| formulas: [f], |
| }); |
| this.state.formulas.push(f); |
| this._requestFrame(body, (json) => { |
| this._addTraces(json, true, false); |
| }); |
| }); |
| }, |
| |
| // Apply scale_by_ave() to all the traces currently being displayed. |
| _scale_by_ave: function() { |
| let promise = this._shortcutAll(); |
| if (!promise) { |
| sk.errorMessage("No traces to scale."); |
| return |
| } |
| promise.then(() => { |
| let f = `scale_by_ave(shortcut("${this.state.keys}"))` |
| this._removeAll(); |
| var body = this._requestFrameBodyFromState(); |
| Object.assign(body, { |
| formulas: [f], |
| }); |
| this.state.formulas.push(f); |
| this._requestFrame(body, (json) => { |
| this._addTraces(json, true, false); |
| }); |
| }); |
| }, |
| |
| _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 != MISSING_DATA_SENTINEL) { |
| values.push([x, y]); |
| } else { |
| values.push([x, null]); |
| } |
| }); |
| 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 != MISSING_DATA_SENTINEL) { |
| values.push([x, y]); |
| } else { |
| values.push([x, null]); |
| } |
| }); |
| 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 = this._requestFrameBodyFromState(); |
| Object.assign(body, { |
| 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 |
| } else { |
| this._requestId = "About to make request"; |
| } |
| 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)); |
| }, |
| |
| // Download all the displayed data as a CSV file. |
| _csv: function() { |
| if (this._csvBlob) { |
| URL.revokeObjectURL(this._csvBlob); |
| this._csvBlob = null; |
| } |
| let csv = []; |
| let line = ['id']; |
| this._dataframe.header.forEach((_,i) => { |
| // TODO(jcgregorio) Look up the git hash and use that as the header. |
| line.push(i); |
| }); |
| csv.push(line.join(',')); |
| for (const traceId in this._dataframe.traceset) { |
| if (traceId === this.ZERO_NAME) { |
| continue |
| } |
| line = [`"${traceId}"`]; |
| this._dataframe.traceset[traceId].forEach((f) => { |
| if (f !== MISSING_DATA_SENTINEL) { |
| line.push(f); |
| } else { |
| line.push(''); |
| } |
| }); |
| csv.push(line.join(',')); |
| } |
| this._csvBlob = new Blob([csv.join('\n')], {type: 'text/csv'}); |
| this.$.csv_download.href = URL.createObjectURL(this._csvBlob); |
| this.$.csv_download.click(); |
| }, |
| |
| _isZero: function(n) { |
| return n === 0; |
| } |
| |
| }); |
| })(); |
| </script> |