blob: aa1cd2d97cee366fea02c2f588a6dfb3ef51373d [file] [log] [blame]
/**
* @module module/explore-multi-sk
* @description <h2><code>explore-multi-sk</code></h2>
*
* Page of Perf for exploring data in multiple graphs.
*
* User is able to add multiple ExploreSimpleSk instances. The state reflector will only
* keep track of those properties necessary to add traces to each graph. All other settings,
* such as the point selected or the beginning and ending range, will be the same
* for all graphs. For example, passing ?dots=true as a URI parameter will enable
* dots for all graphs to be loaded. This is to prevent the URL from becoming too long and
* keeping the logic simple.
*
*/
import { html } from 'lit-html';
import * as query from '../../../infra-sk/modules/query';
import { define } from '../../../elements-sk/modules/define';
import {
DEFAULT_RANGE_S,
ExploreSimpleSk,
State as ExploreState,
} from '../explore-simple-sk/explore-simple-sk';
import { fromKey } from '../paramtools';
import { stateReflector } from '../../../infra-sk/modules/stateReflector';
import { HintableObject } from '../../../infra-sk/modules/hintable';
import { errorMessage } from '../errorMessage';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import '../explore-simple-sk';
const GRAPH_LIMIT = 50;
class State {
begin: number = Math.floor(Date.now() / 1000 - DEFAULT_RANGE_S);
end: number = Math.floor(Date.now() / 1000);
numGraphs: number = 0; // Let's state reflector know how many graphs to add.
graphConfigs: string[] = [];
showZero: boolean = true;
dots: boolean = true;
numCommits: number = 250;
summary: boolean = false;
}
class GraphConfig {
formulas: string[] = []; // Formulas
queries: string[] = []; // Queries
keys: string = ''; // Keys
}
export class ExploreMultiSk extends ElementSk {
private graphConfigs: GraphConfig[] = [];
private exploreElements: ExploreSimpleSk[] = [];
private stateHasChanged: (() => void) | null = null;
private _state: State = new State();
private splitGraphButton: HTMLButtonElement | null = null;
private mergeGraphsButton: HTMLButtonElement | null = null;
constructor() {
super(ExploreMultiSk.template);
}
connectedCallback(): void {
super.connectedCallback();
this._render();
this.splitGraphButton = this.querySelector('#split-graph-button');
this.mergeGraphsButton = this.querySelector('#merge-graphs-button');
this.stateHasChanged = stateReflector(
() => this.state as unknown as HintableObject,
(hintableState) => {
const state = hintableState as unknown as State;
const numElements = this.exploreElements.length;
for (let i = 0; i < state.numGraphs; i++) {
if (i >= numElements) {
this.addEmptyGraph();
}
}
while (this.exploreElements.length > state.numGraphs) {
this.popGraph();
}
this.state = state;
this.updateButtons();
this.exploreElements.forEach((elem, i) => {
const graphConfig = this.graphConfigs[i];
const newState: ExploreState = {
formulas: graphConfig.formulas,
queries: graphConfig.queries,
keys: graphConfig.keys,
begin: state.begin,
end: state.end,
showZero: state.showZero,
dots: state.dots,
numCommits: state.numCommits,
summary: state.summary,
xbaroffset: elem.state.xbaroffset,
autoRefresh: elem.state.autoRefresh,
requestType: elem.state.requestType,
pivotRequest: elem.state.pivotRequest,
sort: elem.state.sort,
selected: elem.state.selected,
_incremental: false,
};
elem.state = newState;
});
}
);
}
private static template = (ele: ExploreMultiSk) => html`
<div id="menu">
<h1>MultiGraph Menu</h1>
<button
@click=${() => {
const explore = ele.addEmptyGraph();
if (explore) {
explore.openQuery();
}
}}
title="Add empty graph.">
Add Graph
</button>
<button
id="split-graph-button"
@click=${() => {
ele.splitGraph();
}}
title="Create multiple graphs from a single graph.">
Split Graph
</button>
<button
id="merge-graphs-button"
@click=${() => {
ele.mergeGraphs();
}}
title="Merge all graphs into a single graph.">
Merge Graphs
</button>
</div>
<hr />
<div id="graphContainer"></div>
`;
private popGraph() {
const graphDiv: Element | null = this.querySelector('#graphContainer');
this.exploreElements.pop();
this.graphConfigs.pop();
this._state.numGraphs -= 1;
this.updateButtons();
graphDiv!.removeChild(graphDiv!.lastChild!);
}
private clearGraphs() {
while (this.exploreElements.length > 0) {
this.popGraph();
}
this._state.numGraphs = 0;
}
private addEmptyGraph(): ExploreSimpleSk | null {
if (this.exploreElements.length >= GRAPH_LIMIT) {
errorMessage(`Cannot exceed display limit of ${GRAPH_LIMIT} graphs.`);
return null;
}
const graphDiv: Element | null = this.querySelector('#graphContainer');
const explore: ExploreSimpleSk = new ExploreSimpleSk();
explore.openQueryByDefault = false;
explore.navOpen = false;
this.exploreElements.push(explore);
this._state.numGraphs += 1;
this.updateButtons();
this.graphConfigs.push(new GraphConfig());
const index = this.exploreElements.length - 1;
explore.addEventListener('state_changed', () => {
const elemState = explore.state;
const graphConfig = this.graphConfigs[index];
graphConfig.formulas = elemState.formulas || [];
graphConfig.queries = elemState.queries || [];
graphConfig.keys = elemState.keys || '';
this.stateHasChanged!();
});
graphDiv!.appendChild(explore);
return explore;
}
public get state(): State {
const graphConfigs: string[] = [];
this.graphConfigs.forEach((config) => {
graphConfigs.push(query.fromObject(config as unknown as HintableObject));
});
return {
...this._state,
graphConfigs: graphConfigs,
};
}
public set state(v: State) {
v.graphConfigs.forEach((config, i) => {
const hintConfig = this.graphConfigs[i] as unknown as HintableObject;
const parsedConfig = query.toObject(
config,
hintConfig
) as unknown as GraphConfig;
this.graphConfigs[i] = {
...this.graphConfigs[i],
...parsedConfig,
};
});
this._state = v;
}
private updateButtons() {
if (this._state.numGraphs === 1) {
this.splitGraphButton!.disabled = false;
} else {
this.splitGraphButton!.disabled = true;
}
if (this._state.numGraphs > 1) {
this.mergeGraphsButton!.disabled = false;
} else {
this.mergeGraphsButton!.disabled = true;
}
}
/**
* Get the trace keys for each graph formatted in a 2D string array.
*
* In the case of formulas, we extract the base key, since all the operations
* we will do on these keys (adding to shortcut store, merging into single graph, etc.)
* are not applicable to function strings. Currently does not support query-based formulas
* (e.g. count(filter("a=b"))).
*
* TODO(@eduardoyap): add support for query-based formulas.
*
* Example output given that we're displaying 2 graphs:
*
* [
* [
* ",a=1,b=2,c=3,",
* ",a=1,b=2,c=4,"
* ],
* [
* ",a=1,b=2,c=5,"
* ]
* ]
*
* @returns {string[][]} - Trace keys.
*/
private getTracesets(): string[][] {
const tracesets: string[][] = [];
this.exploreElements.forEach((elem) => {
const traceset: string[] = [];
const formula_regex = new RegExp(/\((,[^)]+,)\)/);
// Tracesets include traces from Queries and Keys. Traces
// from formulas are wrapped around a formula string.
Object.keys(elem.getTraceset()).forEach((key) => {
if (key[0] === ',') {
traceset.push(key);
} else {
const match = formula_regex.exec(key);
if (match) {
traceset.push(match[1]);
}
}
});
if (traceset.length !== 0) {
tracesets.push(traceset);
}
});
return tracesets;
}
/**
* Parse a structured key into a queries string.
*
* Since this is done on the frontend, this function does not do key or query validation.
*
* Example:
*
* Key ",a=1,b=2,c=3,"
*
* transforms into
*
* Query "a=1&b=2&c=3"
*
* @param {string} key - A structured trace key.
*
* @returns {string} - A query string that can be used in the queries property
* of explore-simple-sk's state.
*/
private queryFromKey(key: string): string {
return new URLSearchParams(fromKey(key)).toString();
}
/**
* Takes the traces of a single graph and create a separate graph for each of those
* traces.
*
* Say the displayed graphs are of the following form:
*
* [
* [
* ",a=1,b=2,c=3,",
* ",a=1,b=2,c=4,",
* ",a=1,b=2,c=5,"
* ],
* ]
*
* The resulting Multigraph structure will be:
*
* [
* [
* ",a=1,b=2,c=3,"
* ],
* [
* ",a=1,b=2,c=4,"
* ],
* [
* ",a=1,b=2,c=5,"
* ]
* ]
*
*
*/
private async splitGraph() {
const traceset = this.getTracesets()[0];
if (!traceset) {
return;
}
this.clearGraphs();
traceset.forEach((key, i) => {
const newExplore = this.addEmptyGraph();
if (newExplore) {
if (key[0] === ',') {
const queries = this.queryFromKey(key);
newExplore.state = {
...newExplore.state,
queries: [queries],
};
this.graphConfigs[i].queries = [queries];
} else {
const formulas = key;
newExplore.state = {
...newExplore.state,
formulas: [formulas],
};
this.graphConfigs[i].formulas = [formulas];
}
}
});
this.stateHasChanged!();
}
/**
*
* Takes the traces of all Graphs and merges them into a single Graph.
*
* Opposite of splitGraph function.
*/
private async mergeGraphs() {
const tracesets = this.getTracesets();
const traces: string[] = [];
// Flatten tracesets
tracesets.forEach((traceset) => {
traceset.forEach((trace) => {
if (!traces.includes(trace)) {
traces.push(trace);
}
});
});
this.clearGraphs();
const newExplore = this.addEmptyGraph();
const queries: string[] = [];
const formulas: string[] = [];
traces.forEach((trace) => {
if (trace[0] === ',') {
queries.push(this.queryFromKey(trace));
} else {
formulas.push(trace);
}
});
newExplore!.state = {
...newExplore!.state,
formulas: formulas,
queries: queries,
};
this.graphConfigs[0].formulas = formulas;
this.graphConfigs[0].queries = queries;
this.stateHasChanged!();
}
}
define('explore-multi-sk', ExploreMultiSk);