blob: ffd1b31cddee086ab35f4acf046e7298cf1cf882 [file] [log] [blame]
/**
* @module modules/trybot-page-sk
* @description <h2><code>trybot-page-sk</code></h2>
*
* This page allows the user to select either a CL or an existing commit in the
* repo to analyze looking for regressions.
*/
import { define } from 'elements-sk/define';
import { html, TemplateResult } from 'lit-html';
import { toParamSet } from 'common-sk/modules/query';
import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
import { stateReflector } from 'common-sk/modules/stateReflector';
import { HintableObject } from 'common-sk/modules/hintable';
import { TabSelectedSkEventDetail } from 'elements-sk/tabs-sk/tabs-sk';
import { SpinnerSk } from 'elements-sk/spinner-sk/spinner-sk';
import { errorMessage } from '../errorMessage';
import { byParams, AveForParam } from '../trybot/calcs';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import {
QuerySk,
QuerySkQueryChangeEventDetail,
} from '../../../infra-sk/modules/query-sk/query-sk';
import { QueryCountSk } from '../query-count-sk/query-count-sk';
import {
ParamSet,
Commit,
TryBotRequest,
TryBotResponse,
Params,
} from '../json';
import { CommitDetailPanelSkCommitSelectedDetails } from '../commit-detail-panel-sk/commit-detail-panel-sk';
import '../../../infra-sk/modules/query-sk';
import '../../../infra-sk/modules/paramset-sk';
import '../query-count-sk';
import '../commit-detail-picker-sk';
import '../day-range-sk';
import '../plot-simple-sk';
import '../window/window';
import 'elements-sk/spinner-sk';
import 'elements-sk/tabs-sk';
import 'elements-sk/tabs-panel-sk';
import 'elements-sk/icon/timeline-icon-sk';
import { PlotSimpleSk, PlotSimpleSkTraceEventDetails } from '../plot-simple-sk/plot-simple-sk';
import { addParamsToParamSet, fromKey, makeKey } from '../paramtools';
import { ParamSetSk } from '../../../infra-sk/modules/paramset-sk/paramset-sk';
import { messagesToErrorString, startRequest } from '../progress/progress';
// Number of elements of a long lists head and tail to display.
const numHeadTail = 10;
// The maximum number of traces to plot in the By Params results.
const maxByParamsPlot = 10;
export class TrybotPageSk extends ElementSk {
private queryCount: QueryCountSk | null = null;
private query: QuerySk | null = null;
private spinner: SpinnerSk | null = null;
private results: TryBotResponse | null = null;
private individualPlot: PlotSimpleSk | null = null
private byParamsPlot: PlotSimpleSk | null = null
private byParams: AveForParam[] = [];
private byParamsTraceID: HTMLParagraphElement | null = null;
private byParamsParamSet: ParamSetSk | null = null;
private state: TryBotRequest = {
kind: 'commit',
cl: '',
patch_number: -1,
commit_number: -1,
query: '',
};
private displayedTrace: boolean = false;
private displayedByParamsTrace: boolean = false;
constructor() {
super(TrybotPageSk.template);
}
private static template = (ele: TrybotPageSk) => html`
<tabs-sk
@tab-selected-sk=${ele.tabSelected}
selected=${ele.state.kind === 'commit' ? 0 : 1}
>
<button>Commit</button>
<button>TryBot</button>
</tabs-sk>
<tabs-panel-sk>
<div>
<h2>Choose which commit to analyze:</h2>
<commit-detail-picker-sk
@commit-selected=${ele.commitSelected}
.selection=${ele.state.commit_number}
id="commit"
></commit-detail-picker-sk>
<h2
?hidden=${ele.state.commit_number === -1}
>Choose the traces to analyze:</h2>
<div
?hidden=${ele.state.commit_number === -1}
class=query
>
<query-sk
id=query
@query-change=${ele.queryChange}
@query-change-delayed=${ele.queryChangeDelayed}
.current_query=${ele.state.query}
></query-sk>
<div class=query-summary>
<paramset-sk
.paramsets=${[toParamSet(ele.state.query)]}
id=summary>
</paramset-sk>
<div class=query-counts>
Matches:
<query-count-sk
id=query-count
url='/_/count/'
@paramset-changed=${ele.paramsetChanged}>
</query-count-sk>
</div>
</div>
</div>
<div class=run>
<button
?hidden=${ele.state.commit_number === -1 || ele.state.query === ''}
@click=${ele.run}
id=run
class=action
>Run</button>
<spinner-sk id=run-spinner></spinner-sk>
</div>
</div>
<div>
TryBot Stuff Goes Here
</div>
</tabs-panel-sk>
<div
class=results
?hidden=${ele.results === null}
>
<h2>Results</h2>
<tabs-sk>
<button>Individual</button>
<button>By Params</button>
</tabs-sk>
<tabs-panel-sk>
<div>
<table>
<tr>
<th>Index</th>
<th title="How many standard deviations this value is from the median.">StdDevs</th>
<th>Plot</th>
${TrybotPageSk.paramKeysAsHeaders(ele)}
</tr>
${TrybotPageSk.individualResults(ele)}
</table>
<p class=tiny>Hold CTRL to add multiple traces to the graph.</p>
<p class=tiny>〃- The value is the same as the trace above it.</p>
<p class=tiny>∅ - The key doesn't appear on this trace.</p>
<plot-simple-sk
id=individual-plot
?hidden=${!ele.displayedTrace}
width="800"
height="250"
></plot-simple-sk>
</div>
<div>
<table>
<tr>
<th>Index</th>
<th>Plot</th>
<th>Param</th>
<th title="How many standard deviations this value is from the median.">StdDevs</th>
<th>N</th>
<th>High</th>
<th>Low</th>
</tr>
${TrybotPageSk.byParamsResults(ele)}
</table>
<p class=tiny>Hold CTRL to add multiple groups of traces to the graph.</p>
<plot-simple-sk
id=by-params-plot
?hidden=${!ele.displayedByParamsTrace}
width="800"
height="250"
@trace_focused=${ele.byParamsTraceFocused}
></plot-simple-sk>
<div
?hidden=${!ele.displayedByParamsTrace}
>
<div
id=by-params-traceid-container
></p><b>TraceID:</b><span id=by-params-traceid></span></div>
<paramset-sk
id=by-params-paramset>
</paramset-sk>
</div>
</div>
</tabs-panel-sk>
</div>
`;
private static paramKeysAsHeaders = (ele: TrybotPageSk): TemplateResult[] | null => {
if (!ele.results) {
return null;
}
const keys = Object.keys(ele.results.paramset);
keys.sort();
return keys.map((key) => html`<th>${key}</th>`);
}
private static individualResults = (ele: TrybotPageSk): TemplateResult[] | null => {
if (!ele.results) {
return null;
}
const keys = Object.keys(ele.results.paramset);
// TODO(jcgregorio) Deduplicate this from here and paramKeysAsHeaders.
keys.sort();
let lastParams: Params = {};
const ret: TemplateResult[] = [];
ele.results.results!.forEach((r, i) => {
// Only display the head and tail of the Individual results.
if (i > numHeadTail && i < ele.results!.results!.length - numHeadTail) {
return;
}
const keyValueDelta: TemplateResult[] = [];
keys.forEach((key) => {
const value = r.params[key];
if (value) {
if (value !== lastParams[key] || i === 0) {
// Highlight values that have changed, but not the first row.
keyValueDelta.push(html`<td>${value}</td>`);
} else {
keyValueDelta.push(html`<td>〃</td>`);
}
} else {
keyValueDelta.push(html`<td title="Does not exists on this trace.">∅</td>`);
}
});
ret.push(html`<tr><td>${i + 1}</td> <td>${r.stddevRatio}</td> <td class=link @click=${(e: MouseEvent) => ele.plotIndividualTrace(e, i)}><timeline-icon-sk></timeline-icon-sk></td> ${keyValueDelta}</tr>`);
lastParams = r.params;
});
return ret;
}
private static byParamsResults = (ele: TrybotPageSk): TemplateResult[] | null => {
if (!ele.byParams) {
return null;
}
const ret: TemplateResult[] = [];
ele.byParams.forEach((b, i) => {
// Only display the head and tail of the byParams results.
if (i > numHeadTail && i < ele.byParams.length - numHeadTail) {
return;
}
ret.push(html`<tr>
<td>${i + 1}</td>
<td class=link @click=${(e: MouseEvent) => ele.plotByParamsTraces(e, i)}><timeline-icon-sk></timeline-icon-sk></td>
<td>${b.keyValue}</td>
<td>${b.aveStdDevRatio}</td>
<td>${b.n}</td>
<td>${b.high}</td>
<td>${b.low}</td>
</tr>`);
});
return ret;
}
async connectedCallback(): Promise<void> {
super.connectedCallback();
this._render();
this.query = this.querySelector('#query');
this.query!.key_order = window.sk.perf.key_order || [];
this.queryCount = this.querySelector('#query-count');
this.spinner = this.querySelector('#run-spinner');
this.individualPlot = this.querySelector('#individual-plot');
this.byParamsPlot = this.querySelector('#by-params-plot');
this.byParamsTraceID = this.querySelector('#by-params-traceid');
this.byParamsParamSet = this.querySelector('#by-params-paramset');
// Populate the query element.
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
try {
const resp = await fetch(`/_/initpage/?tz=${tz}`, {
method: 'GET',
});
const json = await jsonOrThrow(resp);
this.query!.paramset = json.dataframe.paramset;
// From this point on reflect the state to the URL.
this.startStateReflector();
} catch (error) {
errorMessage(error);
}
}
private async run() {
this.displayedTrace = false;
this.displayedByParamsTrace = false;
this.byParamsTraceID!.innerText = '';
this._render();
try {
const prog = await startRequest('/_/trybot/load/', this.state, 200, this.spinner!, null);
if (prog.status === 'Finished') {
this.results = prog.results! as TryBotResponse;
this.byParams = byParams(this.results!);
this._render();
} else {
throw new Error(messagesToErrorString(prog.messages));
}
} catch (error) {
errorMessage(error, 0);
}
}
private getLabels() {
return this.results!.header!.map((colHeader) => new Date(colHeader!.timestamp * 1000));
}
private addZero(lines: {[key: string]: number[]|null}, n: number) {
lines.special_zero = new Array(n).fill(0);
}
private plotIndividualTrace(e: MouseEvent, i: number) {
const result = this.results!.results![i];
const params = result.params;
const lines: {[key: string]: number[]|null} = {};
lines[makeKey(params)] = result.values;
this.addZero(lines, result.values!.length);
if (!e.ctrlKey) {
this.individualPlot!.removeAll();
}
this.individualPlot!.addLines(lines, this.getLabels());
this.displayedTrace = true;
this._render();
}
private plotByParamsTraces(e: MouseEvent, i: number) {
// Pick out the array of traces that match the selected key=value.
const result = this.byParams[i];
const keyValue = result.keyValue;
const [key, value] = keyValue.split('=');
const aveStdDevRatio = result.aveStdDevRatio;
let matches = this.results!.results!.filter((r) => r.params[key] === value);
if (aveStdDevRatio >= 0) {
matches = matches.filter((r) => r.stddevRatio >= 0);
matches.sort((a, b) => b.stddevRatio - a.stddevRatio);
} else {
matches = matches.filter((r) => r.stddevRatio < 0);
matches.sort((a, b) => a.stddevRatio - b.stddevRatio);
}
// Truncate the list.
matches = matches.slice(0, maxByParamsPlot);
const lines: {[key: string]: number[]|null} = {};
matches.forEach((r) => {
lines[makeKey(r.params)] = r.values;
});
if (matches) {
this.addZero(lines, matches[0].values!.length);
}
if (!e.ctrlKey) {
this.byParamsPlot!.removeAll();
}
this.byParamsPlot!.addLines(lines, this.getLabels());
const ps: ParamSet = {};
this.byParamsPlot!.getLineNames().forEach((traceName) => {
addParamsToParamSet(ps, fromKey(traceName));
});
this.byParamsParamSet!.paramsets = [ps];
this.displayedByParamsTrace = true;
this._render();
}
private byParamsTraceFocused(e: CustomEvent<PlotSimpleSkTraceEventDetails>) {
this.byParamsTraceID!.innerText = e.detail.name;
this.byParamsParamSet!.highlight = fromKey(e.detail.name);
}
private tabSelected(e: CustomEvent<TabSelectedSkEventDetail>) {
this.state.kind = e.detail.index === 0 ? 'commit' : 'trybot';
this.stateHasChanged();
}
// Call this anytime something in private state is changed. Will be replaced
// with the real function once stateReflector has been setup.
// eslint-disable-next-line @typescript-eslint/no-empty-function
private stateHasChanged = () => {};
private startStateReflector() {
this.stateHasChanged = stateReflector(
() => (this.state as unknown) as HintableObject,
(state) => {
this.state = (state as unknown) as TryBotRequest;
this._render();
},
);
}
private commitSelected(
e: CustomEvent<CommitDetailPanelSkCommitSelectedDetails>,
) {
this.state.commit_number = ((e.detail.commit as unknown) as Commit).offset;
this.stateHasChanged();
this._render();
}
private paramsetChanged(e: CustomEvent<ParamSet>) {
// The query-count-sk element returned a new paramset for the given query.
this.query!.paramset = e.detail;
}
private queryChangeDelayed(
e: CustomEvent<QuerySkQueryChangeEventDetail>,
) {
// Pass to queryCount so it can update the number of traces that match the
// query.
this.queryCount!.current_query = e.detail.q;
}
private queryChange(e: CustomEvent<QuerySkQueryChangeEventDetail>) {
this.state.query = e.detail.q;
this.stateHasChanged();
this._render();
}
}
define('trybot-page-sk', TrybotPageSk);