blob: 6d1868f066cc0991ff5e7eee32faa75be0d58a56 [file] [log] [blame]
/**
* @module module/explore-sk
* @description <h2><code>explore-sk</code></h2>
*
* Main page of Perf, for exploring data.
*/
import { ElementSk } from '../../../infra-sk/modules/ElementSk'
import { define } from 'elements-sk/define'
import { errorMessage } from 'elements-sk/errorMessage'
import { html, render } from 'lit-html'
import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow'
import { stateReflector } from 'common-sk/modules/stateReflector'
import 'elements-sk/checkbox-sk'
import 'elements-sk/icon/help-icon-sk'
import 'elements-sk/spinner-sk'
import 'elements-sk/styles/buttons'
import 'elements-sk/tabs-panel-sk'
import 'elements-sk/tabs-sk'
import '../commit-detail-panel-sk'
import '../domain-picker-sk'
import '../json-source-sk'
import '../paramset-sk'
import '../plot-simple-sk'
import '../query-count-sk'
import '../query-sk'
import '../query-summary-sk'
// 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;
const ZERO_NAME = "special_zero";
const REFRESH_TIMEOUT = 30*1000; // milliseconds
const DEFAULT_RANGE_S = 24*60*60 // 2 days in seconds.
// TODO(jcgregorio) Move to a 'key' module.
// Returns true if paramName=paramValue appears in the given structured key.
function _matches(key, paramName, paramValue) {
return key.indexOf("," + paramName + "=" + paramValue + ",") >= 0;
};
// TODO(jcgregorio) Move to a 'key' module.
// Parses the structured key and returns a populated object with all
// the param names and values.
function toObject(key) {
let ret = {};
key.split(",").forEach(function(s, i) {
if (i == 0 ) {
return
}
if (s === "") {
return;
}
let parts = s.split("=");
if (parts.length != 2) {
return
}
ret[parts[0]] = parts[1];
});
return ret;
};
const template = (ele) => html`
<div id=buttons>
<domain-picker-sk id=range .state=${ele.state} @domain-changed=${ele._rangeChange}></domain-picker-sk>
<button @click=${ele._removeHighlighted} title="Remove all the highlighted traces.">Remove Highlighted</button>
<button @click=${(e) => ele._removeAll()} title="Remove all the traces.">Remove All</button>
<button @click=${ele._highlightedOnly} title="Remove all but the highlighted traces.">Highlighted Only</button>
<button @click=${ele._clearHighlights} title="Remove highlights from all traces.">Clear Highlights</button>
<button @click=${ele._resetAxes} title="Reset back to the original zoom level.">Reset Axes</button>
<button @click=${ele._shiftLeft} title="Move ${ele._numShift} commits in the past.">&lt;&lt; ${ele._numShift}</button>
<button @click=${ele._shiftBoth} title="Expand the display ${ele._numShift} commits in both directions.">&lt;&lt; ${+ele._numShift} &gt;&gt;</button>
<button @click=${ele._shiftRight} title="Move ${ele._numShift} commits in the future.">${+ele._numShift} &gt;&gt;</button>
<button @click=${ele._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=${ele._isZero(ele._dataframe.skip)} id=skip>${ele._dataframe.skip}</span>
<checkbox-sk name=zero @change=${ele._zeroChangeHandler} ?checked=${ele.state.show_zero} label="Zero" title="Toggle the presence of the zero line.">Zero</checkbox-sk>
<checkbox-sk name=auto @change=${ele._autoRefreshHandler} ?checked=${ele.state.auto_refresh} label="Auto-refresh" title="Auto-refresh the data displayed in the graph.">Auto-Refresh</checkbox-sk>
<button @click=${ele._normalize} title="Apply norm() to all the traces.">Normalize</button>
<button @click=${ele._scale_by_ave} title="Apply scale_by_ave() to all the traces.">Scale By Ave</button>
<button @click=${ele._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>
<spinner-sk id=spinner></spinner-sk>
<span id=percent></span>
</div>
</div>
<plot-simple-sk
width=1024 height=256 id=plot
@trace_selected=${ele._traceSelected}
@zoom=${ele._plotZoom}
@trace_focused=${ele._plotTraceFocused}
></plot-simple-sk>
<div id=tabs>
<tabs-sk id=detailTab>
<button>Query</button>
<button>Params</button>
<button id=commitsTab disabled>Details</button>
</tabs-sk>
<tabs-panel-sk>
<div id=queryTab>
<div class=query-parts>
<query-sk
id=query
@query-change=${ele._queryChangeHandler}
@query-change-delayed=${ele._queryChangeDelayedHandler}
></query-sk>
<div id=selections>
<h3>Selections</h3>
<query-summary-sk id=summary></query-summary-sk>
<div class=query-counts>
Matches: <query-count-sk url='/_/count/' @paramset-changed=${ele._paramsetChanged}>
</query-count-sk>
</div>
<button @click=${ele._add} class=action>Plot</button>
</div>
</div>
<h3>Calculated Traces</h3>
<div class=formulas>
<textarea id=formula rows=3 cols=80></textarea>
<button @click=${ele._addCalculated} class=action>Add</button>
<a href=/help/ target=_blank>
<help-icon-sk></help-icon-sk>
</a>
</div>
</div>
<div>
<p>
<b>Trace ID</b>: <span title="Trace ID" id=trace_id></span>
</p>
<paramset-sk id=paramset clickable_values @paramset-key-value-click=${ele._paramsetKeyValueClick}></paramset-sk>
</div>
<div id=details>
<paramset-sk id=simple_paramset clickable_values @paramset-key-value-click=${ele._paramsetKeyValueClick}></paramset-sk>
<div>
<commit-detail-panel-sk id=commits></commit-detail-panel-sk>
<json-source-sk id=jsonsource></json-source-sk>
</div>
</div>
</tabs-panel-sk>
</div>
`;
define('explore-sk', class extends ElementSk {
constructor() {
super(template);
// Keep track of the data sent to plot.
this._lines = {};
this._dataframe = {
traceset: {},
};
// The state that goes into the URL.
this.state = {
begin: Math.floor(Date.now()/1000 - DEFAULT_RANGE_S),
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.
this._zoomRange = [];
// The id of the current frame request. Will be the empty string if there
// is no pending request.
this._requestId = '';
this._numShift = sk.perf.num_shift;
this._refreshId = -1;
this._csvBlob = null;
this._stateHasChanged = () => {};
}
connectedCallback() {
super.connectedCallback();
if (this._initialized) {
return;
}
this._initialized = true;
this._render();
// TODO(jcgregorio) This is the first time I've missed Polymer's this.$.id notation.
this._commits = this.querySelector('#commits');
this._commitsTab = this.querySelector('#commitsTab');
this._detailTab = this.querySelector('#detailTab');
this._formula = this.querySelector('#formula');
this._jsonsource = this.querySelector('#jsonsource');
this._paramset = this.querySelector('#paramset');
this._percent = this.querySelector('#percent');
this._plot = this.querySelector('#plot');
this._query = this.querySelector('#query');
this._query_count = this.querySelector('query-count-sk');
this._range = this.querySelector('#range');
this._simple_paramset = this.querySelector('#simple_paramset');
this._spinner = this.querySelector('#spinner');
this._summary = this.querySelector('#summary');
this._trace_id = this.querySelector('#trace_id');
this._zoomRange = this.querySelector('#zoomRange');
this._zoom_range = this.querySelector('#zoom_range');
this._csv_download = this.querySelector('#csv_download');
// Populate the query element.
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
fetch('/_/initpage/?tz=' + tz, {
method: 'GET',
}).then(jsonOrThrow).then((json) => {
const now = Math.floor(Date.now()/1000);
this.state.begin = now - 60*60*24;
this.state.end = now;
this._range.state = this.state;
this._query.key_order = sk.perf.key_order;
this._query.paramset = 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();
}).catch(errorMessage);
}
_paramsetChanged(e) {
this._query.paramset = e.detail;
}
_queryChangeDelayedHandler(e) {
this._query_count.current_query = e.detail.q;
}
// Reflect the current query to the query summary.
_queryChangeHandler(e) {
const query = e.detail.q;
this._summary.selection = query;
const 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 + '"');
}
}
// Reflect the focused trace in the paramset.
_plotTraceFocused(e) {
if (e.detail.id === ZERO_NAME) {
return;
}
this._paramset.highlight = toObject(e.detail.id);
this._trace_id.textContent= e.detail.id;
}
// User has zoomed in on the graph.
_plotZoom(e) {
this._zoomRange = [Math.floor(e.detail.xMin/1000), Math.floor(e.detail.xMax/1000)];
this._zoom_range.disabled = false;
}
// Highlight a trace when it is clicked on.
_traceSelected(e) {
this._plot.clearHighlight();
this._plot.setHighlight(e.detail.id);
this._commits.details = [];
const x = +e.detail.pt[0]|0;
// loop backwards from x until you get the next
// non MISSING_DATA_SENTINEL point.
const commits = [this._dataframe.header[x]];
const trace = this._dataframe.traceset[e.detail.id];
for (let 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.
let params = toObject(e.detail.id);
let paramset = {}
Object.keys(params).forEach((key) => {
paramset[key] = [params[key]];
});
// Request populated commits from the server.
fetch('/_/cid/', {
method: 'POST',
body: JSON.stringify(commits),
headers:{
'Content-Type': 'application/json'
}
}).then(jsonOrThrow).then(json => {
this._commits.details = json;
this._commitsTab.disabled = false;
this._simple_paramset.paramsets = {
paramsets: [paramset],
};
this._detailTab.selected = 2;
this._jsonsource.cid = commits[0];
this._jsonsource.traceid = e.detail.id;
}).catch(errorMessage);
}
_startStateReflector() {
this._stateHasChanged = stateReflector(() => this.state, (state) => {
state = this._rationalizeTimeRange(state);
this.state = state;
this._range.state = this.state;
this._render();
this._zeroChanged();
this._autoRefreshChanged();
this._rangeChangeImpl();
});
}
/**
* Fixes up the time ranges in the state that came from query values.
*
* It is possible for the query URL to specify just the begin or end time,
* which may end up giving us an inverted time range, i.e. end < begin.
*/
_rationalizeTimeRange(state) {
if (state.end <= state.begin) {
// If dense then just make sure begin is before end.
if (state.request_type === 1) {
state.begin = state.end - DEFAULT_RANGE_S;
} else {
// If 'begin' was set in the URL.
if (this.state.begin != state.begin) {
state.end = state.begin + DEFAULT_RANGE_S;
} else { // They set 'end' in the URL.
state.begin = states.end - DEFAULT_RANGE_S;
}
}
}
return state;
}
_paramsetKeyValueClick(e) {
const keys = [];
Object.keys(this._lines).forEach((key) => {
if (_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() {
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(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(e) {
this._shiftImpl(-this._numShift, this._numShift);
}
_shiftRight(e) {
this._shiftImpl(this._numShift, this._numShift);
}
_shiftLeft(e) {
this._shiftImpl(-this._numShift, -this._numShift);
}
// Change the current range by the following +/- offsets.
_shiftImpl(beginOffset, endOffset) {
const 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,
}
fetch('/_/shift/', {
method: 'POST',
body: JSON.stringify(body),
headers:{
'Content-Type': 'application/json'
}
}).then(jsonOrThrow).then((json) => {
this.state.begin = json.begin;
this.state.end = json.end;
this.state.num_commits = json.num_commits;
this._rangeChangeImpl();
}).catch(errorMessage);
}
// Fill in the basic data needed for a FrameRequest that will be common
// to all situations.
_requestFrameBodyFromState() {
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() {
let 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() {
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.textContent = '';
}
const body = this._requestFrameBodyFullFromState();
const switchToTab = body.formulas.length > 0 || body.queries.length > 0 || body.keys != "";
this._requestFrame(body, (json) => {
if (json == null) {
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;
this._render();
});
}
_zeroChangeHandler(e) {
this.state.show_zero = e.target.checked;
this._stateHasChanged();
this._zeroChanged();
}
_zeroChanged() {
if (!this._dataframe.header) {
return;
}
if (this.state.show_zero) {
const line = [];
for (let i = 0; i < this._dataframe.header.length; i++) {
line.push([i, 0]);
}
const lines = {};
lines[ZERO_NAME] = line;
this._plot.addLines(lines);
} else {
this._plot.deleteLine(ZERO_NAME);
}
this._plot.resetAxes();
}
_autoRefreshHandler(e) {
this.state.auto_refresh = e.target.checked;
this._stateHasChanged();
this._autoRefreshChanged();
}
_autoRefreshChanged() {
if (!this.state.auto_refresh) {
if (this._refreshId != -1) {
clearInterval(this._refreshId);
}
} else {
this._refreshId = setInterval(() => this._autoRefresh(), REFRESH_TIMEOUT);
}
}
_autoRefresh() {
// Update end to be now.
this.state.end = Math.floor(Date.now()/1000);
const body = this._requestFrameBodyFullFromState();
const 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(json, incremental, tab) {
const dataframe = json.dataframe;
const lines = {};
if (dataframe.traceset === null) {
return;
}
// Add in the 0-trace.
if (this.state.show_zero) {
dataframe.traceset[ZERO_NAME] = Array(dataframe.header.length).fill(0);
}
// Convert the dataframe into a form suitable for the plot element.
const keys = Object.keys(dataframe.traceset);
keys.forEach((key) => {
const 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((key) => {
this._lines[key] = lines[key];
});
if (this._dataframe.header === undefined) {
this._dataframe = dataframe;
} else {
Object.keys(dataframe.traceset).forEach((key) => {
this._dataframe.traceset[key] = dataframe.traceset[key];
});
}
} else {
this._lines = lines;
this._dataframe = dataframe;
}
if (!incremental) {
this._plot.removeAll();
}
const 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);
const bands = [];
for (let 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) {
let xbaroffset = this.state.xbaroffset;
let xbar = -1;
this._dataframe.header.forEach((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.paramsets = {
paramsets: [dataframe.paramset],
};
if (tab) {
this._detailTab.selected = 1;
}
}
_add() {
const q = this._query.current_query.trim();
if (q == "") {
return
}
if (this.state.queries.indexOf(q) == -1) {
this.state.queries.push(q);
}
let body = this._requestFrameBodyFromState();
Object.assign(body, {
queries: [q],
});
this._requestFrame(body, (json) => {
this._addTraces(json, true, true);
});
}
/**
* Removes all traces.
*
* @param skipHistory {Boolean} - If true then don't update the URL. Used
* in calls like _normalize() where this is just an intermediate state we
* don't want in history.
*/
_removeAll(skipHistory) {
this.state.formulas = [];
this.state.queries = [];
this.state.keys = "";
this._plot.removeAll();
this._lines = [];
if (!skipHistory) {
this._stateHasChanged();
}
}
// 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(keys) {
if (keys.length == 0) {
this.state.keys = "";
this.state.queries = [];
return undefined;
}
const state = {
keys: keys,
};
return fetch('/_/keys/', {
method: 'POST',
body: JSON.stringify(state),
headers:{
'Content-Type': 'application/json'
}
}).then(jsonOrThrow).then((json) => {
this.state.keys = json.id;
this.state.queries = [];
this._stateHasChanged();
}).catch(errorMessage);
}
// 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() {
const toShortcut = [];
Object.keys(this._dataframe.traceset).forEach((key) => {
if (key[0] == ",") {
toShortcut.push(key);
}
});
return this._reShortCut(toShortcut);
}
// Apply norm() to all the traces currently being displayed.
_normalize() {
let promise = this._shortcutAll();
if (!promise) {
errorMessage("No traces to normalize.");
return;
}
promise.then(() => {
let f = `norm(shortcut("${this.state.keys}"))`
this._removeAll(true);
let body = this._requestFrameBodyFromState();
Object.assign(body, {
formulas: [f],
});
this.state.formulas.push(f);
this._stateHasChanged();
this._requestFrame(body, (json) => {
this._addTraces(json, true, false);
});
});
}
// Apply scale_by_ave() to all the traces currently being displayed.
_scale_by_ave() {
let promise = this._shortcutAll();
if (!promise) {
errorMessage("No traces to scale.");
return;
}
promise.then(() => {
let f = `scale_by_ave(shortcut("${this.state.keys}"))`
this._removeAll(true);
var body = this._requestFrameBodyFromState();
Object.assign(body, {
formulas: [f],
});
this.state.formulas.push(f);
this._stateHasChanged();
this._requestFrame(body, (json) => {
this._addTraces(json, true, false);
});
});
}
_removeHighlighted() {
const ids = this._plot.highlighted();
const toadd = {};
const toShortcut = [];
Object.keys(this._dataframe.traceset).forEach((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);
}
const values = [];
this._dataframe.traceset[key].forEach((y, x) => {
if (y != MISSING_DATA_SENTINEL) {
values.push([x, y]);
} else {
values.push([x, null]);
}
});
toadd[key] = values;
});
// Remove the traces from the traceset so they don't reappear.
ids.forEach((key) => {
if (this._dataframe.traceset[key] != undefined) {
delete this._dataframe.traceset[key];
}
});
this._lines = toadd;
this._plot.removeAll();
this._plot.addLines(toadd);
this._reShortCut(toShortcut);
}
_highlightedOnly() {
const ids = this._plot.highlighted();
const toadd = {};
const toremove = [];
const toShortcut = [];
Object.keys(this._dataframe.traceset).forEach((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);
}
const values = [];
this._dataframe.traceset[key].forEach((y, x) => {
if (y != MISSING_DATA_SENTINEL) {
values.push([x, y]);
} else {
values.push([x, null]);
}
});
toadd[key] = values;
});
// Remove the traces from the traceset so they don't reappear.
Object.keys(this._dataframe.traceset).forEach((key) => {
if (key in toremove) {
delete this._dataframe.traceset[key];
}
});
this._lines = toadd;
this._plot.removeAll();
this._plot.addLines(toadd);
this._reShortCut(toShortcut);
}
_clearHighlights() {
this._plot.clearHighlight();
}
_resetAxes() {
this._plot.resetAxes();
this._zoom_range.disabled = true;
}
_addCalculated() {
const f = this._formula.value;
if (f.trim() === '') {
return;
}
if (this.state.formulas.indexOf(f) === -1) {
this.state.formulas.push(f);
}
let 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(msg) {
this._requestId = '';
if (msg) {
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(body, cb) {
body.tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (this._requestId != '') {
errorMessage('There is a pending query already running.');
return;
} else {
this._requestId = 'About to make request';
}
this._spinner.active = true;
this._stateHasChanged();
fetch('/_/frame/start', {
method: 'POST',
body: JSON.stringify(body),
headers:{
'Content-Type': 'application/json'
}
}).then(jsonOrThrow).then((json) => {
this._requestId = json.id;
this._checkFrameRequestStatus(cb);
}).catch((msg) => this._catch(msg));
}
// Periodically check the status of a pending FrameRequest, calling the
// 'cb' callback with the decoded JSON upon success.
_checkFrameRequestStatus(cb) {
fetch(`/_/frame/status/${this._requestId}`, {
method: 'GET',
}).then(jsonOrThrow).then((json) => {
if (json.state === 'Running') {
this._percent.textContent = Math.floor(json.percent*100) + '%';
window.setTimeout(() => this._checkFrameRequestStatus(cb), 300);
} else {
fetch(`/_/frame/results/${this._requestId}`, {
method: 'GET',
}).then(jsonOrThrow).then((json) => {
cb(json);
this._catch(json.msg);
}).catch((msg) => this._catch(msg));
}
}).catch((msg) => this._catch(msg));
}
// Download all the displayed data as a CSV file.
_csv() {
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 === 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(n) {
return n === 0;
}
});