| /** |
| * @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); |