blob: 72400f6a39c8a855761e681a282b8474bd6ad6ff [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 { define } from '../../../elements-sk/modules/define';
import {
DEFAULT_RANGE_S,
ExploreSimpleSk,
State as ExploreState,
GraphConfig,
LabelMode,
updateShortcut,
} 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';
import '../../../golden/modules/pagination-sk/pagination-sk';
import { jsonOrThrow } from '../../../infra-sk/modules/jsonOrThrow';
import { PaginationSkPageChangedEventDetail } from '../../../golden/modules/pagination-sk/pagination-sk';
class State {
begin: number = Math.floor(Date.now() / 1000 - DEFAULT_RANGE_S);
end: number = Math.floor(Date.now() / 1000);
shortcut: string = '';
showZero: boolean = true;
dots: boolean = true;
numCommits: number = 250;
summary: boolean = false;
pageSize: number = 10;
pageOffset: number = 0;
totalGraphs: number = 0;
}
export class ExploreMultiSk extends ElementSk {
private graphConfigs: GraphConfig[] = [];
private exploreElements: ExploreSimpleSk[] = [];
private currentPageExploreElements: ExploreSimpleSk[] = [];
private currentPageGraphConfigs: GraphConfig[] = [];
private stateHasChanged: (() => void) | null = null;
private _state: State = new State();
private splitGraphButton: HTMLButtonElement | null = null;
private mergeGraphsButton: HTMLButtonElement | null = null;
private graphDiv: Element | null = null;
constructor() {
super(ExploreMultiSk.template);
}
connectedCallback(): void {
super.connectedCallback();
this._render();
this.graphDiv = this.querySelector('#graphContainer');
this.splitGraphButton = this.querySelector('#split-graph-button');
this.mergeGraphsButton = this.querySelector('#merge-graphs-button');
this.stateHasChanged = stateReflector(
() => this.state as unknown as HintableObject,
async (hintableState) => {
const state = hintableState as unknown as State;
const numElements = this.exploreElements.length;
let graphConfigs: GraphConfig[] | undefined = [];
if (state.shortcut !== '') {
graphConfigs = await this.getConfigsFromShortcut(state.shortcut);
if (graphConfigs === undefined) {
graphConfigs = [];
}
}
// This loop helps get rid of extra graphs that aren't part of the
// current config. A scenario where this occurs is if we have 1 graph,
// add another graph and then go back in the browser.
while (this.exploreElements.length > graphConfigs.length) {
this.exploreElements.pop();
this.graphConfigs.pop();
this.graphDiv!.removeChild(this.graphDiv!.lastChild!);
}
for (let i = 0; i < graphConfigs.length; i++) {
if (i >= numElements) {
this.addEmptyGraph();
}
this.graphConfigs[i] = graphConfigs[i];
}
this.state = state;
this.addGraphsToCurrentPage();
this.updateButtons();
}
);
}
private static template = (ele: ExploreMultiSk) => html`
<div id="menu">
<h1>MultiGraph Menu</h1>
<button
@click=${() => {
const explore = ele.addEmptyGraph();
if (explore) {
ele.updatePageForNewExplore();
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 />
<pagination-sk
offset=${ele.state.pageOffset}
page_size=${ele.state.pageSize}
total=${ele.state.totalGraphs}
@page-changed=${ele.pageChanged}>
</pagination-sk>
<label>
<span class="prefix">Charts per page</span>
<input
@change=${ele.pageSizeChanged}
type="number"
.value="${ele.state.pageSize.toString()}"
min="1"
max="50"
title="The number of charts per page." />
</label>
<div id="graphContainer"></div>
<pagination-sk
offset=${ele.state.pageOffset}
page_size=${ele.state.pageSize}
total=${ele.state.totalGraphs}
@page-changed=${ele.pageChanged}>
</pagination-sk>
`;
private clearGraphs() {
this.exploreElements = [];
this.graphConfigs = [];
this.updateButtons();
}
private emptyCurrentPage(): void {
while (this.graphDiv!.hasChildNodes()) {
this.graphDiv!.removeChild(this.graphDiv!.lastChild!);
}
this.currentPageExploreElements = [];
this.currentPageGraphConfigs = [];
}
private addGraphsToCurrentPage(): void {
this.state.totalGraphs = this.exploreElements.length;
this.emptyCurrentPage();
const startIndex = this.state.pageOffset;
let endIndex = startIndex + this.state.pageSize - 1;
if (this.exploreElements.length <= endIndex) {
endIndex = this.exploreElements.length - 1;
}
for (let i = startIndex; i <= endIndex; i++) {
this.graphDiv!.appendChild(this.exploreElements[i]);
this.currentPageExploreElements.push(this.exploreElements[i]);
this.currentPageGraphConfigs.push(this.graphConfigs[i]);
}
this.currentPageExploreElements.forEach((elem, i) => {
const graphConfig = this.currentPageGraphConfigs[i];
this.addStateToExplore(elem, graphConfig);
});
this._render();
}
private addStateToExplore(
explore: ExploreSimpleSk,
graphConfig: GraphConfig
) {
const newState: ExploreState = {
formulas: graphConfig.formulas || [],
queries: graphConfig.queries || [],
keys: graphConfig.keys || '',
begin: this.state.begin,
end: this.state.end,
showZero: this.state.showZero,
dots: this.state.dots,
numCommits: this.state.numCommits,
summary: this.state.summary,
xbaroffset: explore.state.xbaroffset,
autoRefresh: explore.state.autoRefresh,
requestType: explore.state.requestType,
pivotRequest: explore.state.pivotRequest,
sort: explore.state.sort,
selected: explore.state.selected,
_incremental: false,
labelMode: LabelMode.Date,
disable_filter_parent_traces: explore.state.disable_filter_parent_traces,
};
explore.state = newState;
}
private addEmptyGraph(): ExploreSimpleSk | null {
const explore: ExploreSimpleSk = new ExploreSimpleSk(true);
explore.openQueryByDefault = false;
explore.navOpen = false;
this.exploreElements.push(explore);
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.updateShortcutMultiview();
});
return explore;
}
public get state(): State {
return this._state;
}
public set state(v: State) {
this._state = v;
}
private updateButtons() {
if (this.exploreElements.length === 1) {
this.splitGraphButton!.disabled = false;
} else {
this.splitGraphButton!.disabled = true;
}
if (this.exploreElements.length > 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) => {
this.addEmptyGraph();
if (key[0] === ',') {
const queries = this.queryFromKey(key);
this.graphConfigs[i].queries = [queries];
} else {
const formulas = key;
this.graphConfigs[i].formulas = [formulas];
}
});
this.updateShortcutMultiview();
// Upon the split action, we would want to move to the first page
// of the split graph set.
this.state.pageOffset = 0;
this.addGraphsToCurrentPage();
}
/**
*
* Takes the traces of all Graphs and merges them into a single Graph.
*
* Opposite of splitGraph function.
*/
private async mergeGraphs() {
const mergedGraphConfig = new GraphConfig();
this.graphConfigs.forEach((config) => {
config.formulas.forEach((formula) => {
mergedGraphConfig.formulas.push(formula);
});
config.queries.forEach((query) => {
mergedGraphConfig.queries.push(query);
});
});
this.clearGraphs();
this.addEmptyGraph();
this.graphConfigs[0] = mergedGraphConfig;
this.updateShortcutMultiview!();
// Upon the merge action, we would want to move to the first page.
this.state.pageOffset = 0;
this.addGraphsToCurrentPage();
}
/**
* Fetches the Graph Configs in the DB for a given shortcut ID.
*
* @param {string} shortcut - shortcut ID to look for in the GraphsShortcut table.
* @returns - List of Graph Configs matching the shortcut ID in the GraphsShortcut table
* or undefined if the ID doesn't exist.
*/
private getConfigsFromShortcut(
shortcut: string
): Promise<GraphConfig[]> | undefined {
const body = {
ID: shortcut,
};
return fetch('/_/shortcut/get', {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
})
.then(jsonOrThrow)
.then((json) => json.graphs)
.catch(errorMessage);
}
/**
* Creates a shortcut ID for the current Graph Configs and updates the state.
*
*/
private updateShortcutMultiview() {
updateShortcut(this.graphConfigs)
.then((shortcut) => {
if (shortcut === '') {
this.state.shortcut = '';
this.stateHasChanged!();
return;
}
this.state.shortcut = shortcut;
this.stateHasChanged!();
})
.catch(errorMessage);
}
private pageChanged(e: CustomEvent<PaginationSkPageChangedEventDetail>) {
this.state.pageOffset = Math.max(
0,
this.state.pageOffset + e.detail.delta * this.state.pageSize
);
this.stateHasChanged!();
this.addGraphsToCurrentPage();
}
private pageSizeChanged(e: MouseEvent) {
this.state.pageSize = +(e.target! as HTMLInputElement).value;
this.stateHasChanged!();
this.addGraphsToCurrentPage();
}
private updatePageForNewExplore() {
// Check if there is space left on the current page
if (this.graphDiv!.childElementCount === this.state.pageSize) {
// We will have to add another page since the current one is full.
// Go to the next page.
this.pageChanged(
new CustomEvent<PaginationSkPageChangedEventDetail>('page-changed', {
detail: {
delta: 1,
},
bubbles: true,
})
);
} else {
// Re-render the page
this.addGraphsToCurrentPage();
}
}
}
define('explore-multi-sk', ExploreMultiSk);