| /** |
| * @module module/cluster-page-sk |
| * @description <h2><code>cluster-page-sk</code></h2> |
| * |
| * The top level element for clustering traces. |
| * |
| */ |
| import { define } from 'elements-sk/define'; |
| import { errorMessage } from 'elements-sk/errorMessage'; |
| import { fromObject, toParamSet } from 'common-sk/modules/query'; |
| import { html } from 'lit-html'; |
| import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow'; |
| import { stateReflector } from 'common-sk/modules/stateReflector'; |
| import { ElementSk } from '../../../infra-sk/modules/ElementSk'; |
| |
| import 'elements-sk/spinner-sk'; |
| import 'elements-sk/checkbox-sk'; |
| import 'elements-sk/styles/buttons'; |
| |
| import '../../../infra-sk/modules/paramset-sk'; |
| import '../../../infra-sk/modules/sort-sk'; |
| import '../../../infra-sk/modules/query-sk'; |
| |
| import '../algo-select-sk'; |
| import '../cluster-summary2-sk'; |
| import '../commit-detail-picker-sk'; |
| import '../day-range-sk'; |
| import '../query-count-sk'; |
| import { |
| FrameResponse, |
| ParamSet, |
| RegressionDetectionRequest, |
| ClusterAlgo, |
| CommitID, |
| RangeRequest, |
| CommitDetail, |
| ClusterStartResponse, |
| ClusterStatus, |
| FullSummary, |
| RegressionDetectionResponse, |
| } from '../json'; |
| import { HintableObject } from 'common-sk/modules/hintable'; |
| import { AlgoSelectAlgoChangeEventDetail } from '../algo-select-sk/algo-select-sk'; |
| import { QuerySkQueryChangeEventDetail } from '../../../infra-sk/modules/query-sk/query-sk'; |
| import { ClusterSummary2SkOpenKeysEventDetail } from '../cluster-summary2-sk/cluster-summary2-sk'; |
| import { DayRangeSkChangeDetail } from '../day-range-sk/day-range-sk'; |
| import { CommitDetailPanelSkCommitSelectedDetails } from '../commit-detail-panel-sk/commit-detail-panel-sk'; |
| |
| // The state that gets reflected to the URL. |
| class State { |
| begin: number; |
| end: number; |
| offset: number; |
| radius: number; |
| query: string; |
| k: number; |
| algo: ClusterAlgo; |
| interesting: number; |
| sparse: boolean; |
| |
| constructor() { |
| this.begin = Math.floor(Date.now() / 1000 - 24 * 60 * 60); |
| this.end = Math.floor(Date.now() / 1000); |
| if (window.sk.perf.demo) { |
| this.begin = Math.floor(new Date(2020, 4, 1).valueOf() / 1000); |
| this.end = Math.floor(new Date(2020, 5, 1).valueOf() / 1000); |
| } |
| this.offset = -1; |
| this.radius = window.sk.perf.radius; |
| this.query = ''; |
| this.k = 0; |
| this.algo = 'kmeans'; |
| this.interesting = window.sk.perf.interesting; |
| this.sparse = false; |
| } |
| } |
| |
| // The date range over which commits are presented. |
| interface Range { |
| begin: number | null; |
| end: number | null; |
| } |
| |
| export class ClusterPageSk extends ElementSk { |
| private static template = (ele: ClusterPageSk) => html` |
| <h2>Commit</h2> |
| <h3>Appears in Date Range</h3> |
| <div class="day-range-with-spinner"> |
| <day-range-sk |
| id="range" |
| @day-range-change=${ele.rangeChange} |
| begin=${ele.state.begin} |
| end=${ele.state.end} |
| ></day-range-sk> |
| <spinner-sk ?active=${ele.updatingCommits}></spinner-sk> |
| </div> |
| <h3>Commit</h3> |
| <div> |
| <commit-detail-picker-sk |
| @commit-selected=${ele.commitSelected} |
| .selected=${ele.selectedCommitIndex} |
| .details=${ele.cids} |
| id="commit" |
| ></commit-detail-picker-sk> |
| </div> |
| |
| <h2>Algorithm</h2> |
| <algo-select-sk |
| algo=${ele.state.algo} |
| @algo-change=${ele.algoChange} |
| ></algo-select-sk> |
| |
| <h2>Query</h2> |
| <div class="query-action"> |
| <query-sk |
| @query-change=${ele.queryChanged} |
| .key_order=${window.sk.perf.key_order} |
| .paramset=${ele.paramset} |
| current_query=${ele.state.query} |
| ></query-sk> |
| <div id="selections"> |
| <h3>Selections</h3> |
| <paramset-sk |
| id="summary" |
| .paramsets=${[toParamSet(ele.state.query)]} |
| ></paramset-sk> |
| <div> |
| Matches: |
| <query-count-sk |
| url="/_/count/" |
| current_query=${ele.state.query} |
| @paramset-changed=${ele.paramsetChanged} |
| ></query-count-sk> |
| </div> |
| <button |
| @click=${ele.start} |
| class="action" |
| id="start" |
| ?disabled=${!!ele.requestId} |
| > |
| Run |
| </button> |
| <div> |
| <spinner-sk ?active=${!!ele.requestId}></spinner-sk> |
| <span>${ele.status}</span> |
| </div> |
| </div> |
| </div> |
| |
| <details> |
| <summary id="advanced"> |
| <h2>Advanced</h2> |
| </summary> |
| <div id="inputs"> |
| <label> |
| K (A value of 0 means the server chooses). |
| <input .value=${ele.state.k} @input=${ele.kChange} /> |
| </label> |
| <label> |
| Number of commits to include on either side. |
| <input .value=${ele.state.radius} @input=${ele.radiusChange} /> |
| </label> |
| <label> |
| Clusters are interesting if regression score >= this. |
| <input |
| .value=${ele.state.interesting} |
| @input=${ele.interestingChange} |
| /> |
| </label> |
| <checkbox-sk |
| ?checked=${ele.state.sparse} |
| label="Data is sparse, so only include commits that have data." |
| @input=${ele.sparseChange} |
| ></checkbox-sk> |
| </div> |
| </details> |
| |
| <h2>Results</h2> |
| <sort-sk target="clusters"> |
| <button data-key="clustersize">Cluster Size</button> |
| <button data-key="stepregression" data-default="up">Regression</button> |
| <button data-key="stepsize">Step Size</button> |
| <button data-key="steplse">Least Squares</button> |
| </sort-sk> |
| <div id="clusters" @open-keys=${ele.openKeys}> |
| ${ClusterPageSk._summaryRows(ele)} |
| </div> |
| `; |
| |
| private static _summaryRows = (ele: ClusterPageSk) => { |
| const ret = ele.summaries.map( |
| (summary) => |
| html` |
| <cluster-summary2-sk |
| .full_summary=${summary} |
| notriage |
| ></cluster-summary2-sk> |
| ` |
| ); |
| if (!ret.length) { |
| ret.push(html` |
| <p class="info"> |
| No clusters found. |
| </p> |
| `); |
| } |
| return ret; |
| }; |
| |
| // The state to be reflected to the URL. |
| private state = new State(); |
| |
| private paramset: ParamSet = {}; |
| |
| // The computed clusters. |
| private summaries: FullSummary[] = []; |
| |
| // The commits to choose from. |
| private cids: CommitDetail[] = []; |
| |
| // Which commit is selected. |
| private selectedCommitIndex: number = -1; |
| |
| // The id of the current cluster request. Will be the empty string if |
| // there is no pending request. |
| private requestId: string = ''; |
| |
| // The status of a running request. |
| private status: string = ''; |
| |
| // True if we are fetching a new list of _cids from the server. |
| private updatingCommits: boolean = false; |
| |
| // Only update _cids if the date range is different from the last fetch. |
| private lastRange: Range = { |
| begin: null, |
| end: null, |
| }; |
| |
| // Call this anytime something in private state is changed. Will be replaced |
| // with the real function once stateReflector has been setup. |
| // tslint:disable-next-line: no-empty |
| private stateHasChanged = () => {}; |
| |
| constructor() { |
| super(ClusterPageSk.template); |
| } |
| |
| connectedCallback() { |
| super.connectedCallback(); |
| this._render(); |
| |
| const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; |
| fetch(`/_/initpage/?tz=${tz}`) |
| .then(jsonOrThrow) |
| .then((json: FrameResponse) => { |
| this.paramset = json.dataframe!.paramset; |
| this._render(); |
| }) |
| .catch(errorMessage); |
| |
| this.stateHasChanged = stateReflector( |
| () => (this.state as unknown) as HintableObject, |
| (state) => { |
| this.state = (state as unknown) as State; |
| this._render(); |
| this.updateCommitSelections(); |
| } |
| ); |
| } |
| |
| private algoChange(e: CustomEvent<AlgoSelectAlgoChangeEventDetail>) { |
| this.state.algo = e.detail.algo; |
| this.stateHasChanged(); |
| } |
| |
| private kChange(e: InputEvent) { |
| this.state.k = +(e.target! as HTMLInputElement).value; |
| this.stateHasChanged(); |
| } |
| |
| private radiusChange(e: InputEvent) { |
| this.state.radius = +(e.target! as HTMLInputElement).value; |
| this.stateHasChanged(); |
| } |
| |
| private interestingChange(e: InputEvent) { |
| this.state.interesting = +(e.target! as HTMLInputElement).value; |
| this.stateHasChanged(); |
| } |
| |
| private sparseChange(e: InputEvent) { |
| this.state.sparse = (e.target! as HTMLInputElement).checked; |
| this.stateHasChanged(); |
| } |
| |
| private queryChanged(e: CustomEvent<QuerySkQueryChangeEventDetail>) { |
| this.state.query = e.detail.q; |
| this.stateHasChanged(); |
| this._render(); |
| } |
| |
| private paramsetChanged(e: CustomEvent<ParamSet>) { |
| this.paramset = e.detail; |
| this._render(); |
| } |
| |
| private openKeys(e: CustomEvent<ClusterSummary2SkOpenKeysEventDetail>) { |
| const query = { |
| keys: e.detail.shortcut, |
| begin: e.detail.begin, |
| end: e.detail.end, |
| xbaroffset: e.detail.xbar.offset, |
| num_commits: 50, |
| request_type: 1, |
| }; |
| window.open(`/e/?${fromObject(query)}`, '_blank'); |
| } |
| |
| private rangeChange(e: CustomEvent<DayRangeSkChangeDetail>) { |
| this.state.begin = e.detail.begin; |
| this.state.end = e.detail.end; |
| this.stateHasChanged(); |
| this.updateCommitSelections(); |
| } |
| |
| private commitSelected( |
| e: CustomEvent<CommitDetailPanelSkCommitSelectedDetails> |
| ) { |
| this.state.offset = ((e.detail.commit as unknown) as CommitID).offset; |
| this.stateHasChanged(); |
| } |
| |
| private updateCommitSelections() { |
| if ( |
| this.lastRange.begin === this.state.begin && |
| this.lastRange.end === this.state.end |
| ) { |
| return; |
| } |
| this.lastRange = { |
| begin: this.state.begin, |
| end: this.state.end, |
| }; |
| const body: RangeRequest = { |
| begin: this.state.begin, |
| end: this.state.end, |
| offset: this.state.offset, |
| }; |
| this.updatingCommits = true; |
| fetch('/_/cidRange/', { |
| method: 'POST', |
| body: JSON.stringify(body), |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| }) |
| .then(jsonOrThrow) |
| .then((cids: CommitDetail[]) => { |
| this.updatingCommits = false; |
| cids.reverse(); |
| this.cids = cids; |
| |
| this.selectedCommitIndex = -1; |
| // Look for commit id in this._cids. |
| for (let i = 0; i < cids.length; i++) { |
| if ( |
| // TODO(jcgregorio) Fix how go2ts handles nested structs. |
| ((cids[i] as unknown) as CommitID).offset === this.state.offset |
| ) { |
| this.selectedCommitIndex = i; |
| break; |
| } |
| } |
| |
| if (!this.state.begin) { |
| this.state.begin = cids[cids.length - 1].ts; |
| this.state.end = cids[0].ts; |
| } |
| this._render(); |
| }) |
| .catch((msg) => { |
| if (msg) { |
| errorMessage(msg, 10000); |
| } |
| this.updatingCommits = false; |
| this._render(); |
| }); |
| } |
| |
| private catch(msg: string) { |
| this.requestId = ''; |
| this.status = ''; |
| if (msg) { |
| errorMessage(msg, 10000); |
| } |
| this._render(); |
| } |
| |
| private checkClusterRequestStatus( |
| cb: (summaries: RegressionDetectionResponse) => void |
| ) { |
| fetch(`/_/cluster/status/${this.requestId}`) |
| .then(jsonOrThrow) |
| .then((json: ClusterStatus) => { |
| if (json.state === 'Running') { |
| this.status = json.message; |
| this._render(); |
| window.setTimeout(() => this.checkClusterRequestStatus(cb), 300); |
| } else { |
| if (json.value) { |
| cb(json.value); |
| } |
| this.catch(json.message); |
| } |
| }) |
| .catch((msg) => this.catch(msg)); |
| } |
| |
| private start() { |
| if (this.requestId) { |
| errorMessage('There is a pending query already running.'); |
| return; |
| } |
| const body: RegressionDetectionRequest = { |
| query: this.state.query, |
| step: 0, |
| total_queries: 0, |
| alert: { |
| id: -1, |
| id_as_string: '-1', |
| display_name: '', |
| radius: +this.state.radius, |
| query: this.state.query, |
| k: +this.state.k, |
| algo: this.state.algo, |
| interesting: +this.state.interesting, |
| sparse: this.state.sparse, |
| step: '', |
| alert: '', |
| bug_uri_template: '', |
| state: 'ACTIVE', |
| owner: '', |
| step_up_only: false, |
| direction: 'BOTH', |
| group_by: '', |
| minimum_num: 0, |
| category: '', |
| }, |
| domain: { |
| offset: +this.state.offset, |
| n: 0, |
| end: new Date().toISOString(), |
| }, |
| }; |
| this.summaries = []; |
| // Set a value for _requestId so the spinner starts, and we don't start |
| // another request too soon. |
| this.requestId = 'pending'; |
| this._render(); |
| fetch('/_/cluster/start', { |
| method: 'POST', |
| body: JSON.stringify(body), |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| }) |
| .then(jsonOrThrow) |
| .then((json: ClusterStartResponse) => { |
| this.requestId = json.id; |
| this.checkClusterRequestStatus( |
| (regressionDetectionResponse: RegressionDetectionResponse) => { |
| this.summaries = []; |
| regressionDetectionResponse.summary!.Clusters!.forEach( |
| (clusterSummary) => { |
| this.summaries.push({ |
| summary: clusterSummary!, |
| frame: regressionDetectionResponse.frame!, |
| triage: { |
| status: '', |
| message: '', |
| }, |
| }); |
| } |
| ); |
| this._render(); |
| } |
| ); |
| }) |
| .catch((msg) => this.catch(msg)); |
| } |
| } |
| |
| define('cluster-page-sk', ClusterPageSk); |