| /** |
| * @module modules/cluster-summary2-sk |
| * @description <h2><code>cluster-summary2-sk</code></h2> |
| * |
| * @evt open-keys - An event that is fired when the user clicks the 'View on |
| * dashboard' button that contains the shortcut id, and the timestamp range of |
| * traces in the details that should be opened in the explorer, and the xbar |
| * location specified as a serialized cid.CommitID, for example: |
| * |
| * { |
| * shortcut: 'X1129832198312', |
| * begin: 1476982874, |
| * end: 1476987166, |
| * xbar: {'offset':24750,'timestamp':1476985844}, |
| * } |
| * |
| * @evt triaged - An event generated when the 'Update' button is pressed, which |
| * contains the new triage status. The detail contains the cid and triage |
| * status, for example: |
| * |
| * { |
| * cid: { |
| * source: 'master', |
| * offset: 25004, |
| * }, |
| * triage: { |
| * status: 'negative', |
| * messge: 'This is a regression in ...', |
| * }, |
| * } |
| * |
| * @attr {Boolean} notriage - If true then don't display the triage controls. |
| * |
| * @example |
| */ |
| import { define } from 'elements-sk/define'; |
| import { html } from 'lit-html'; |
| import 'elements-sk/styles/buttons'; |
| import 'elements-sk/collapse-sk'; |
| import '../commit-detail-panel-sk'; |
| import '../plot-simple-sk'; |
| import '../triage2-sk'; |
| import '../word-cloud-sk'; |
| import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow'; |
| import { CollapseSk } from 'elements-sk/collapse-sk/collapse-sk'; |
| import { errorMessage } from '../errorMessage'; |
| import { ElementSk } from '../../../infra-sk/modules/ElementSk'; |
| import { Login } from '../../../infra-sk/modules/login'; |
| import { |
| FullSummary, |
| FrameResponse, |
| ClusterSummary, |
| TriageStatus, |
| Commit, |
| CommitNumber, |
| Status, |
| ColumnHeader, |
| Alert, |
| StepDetection, |
| } from '../json'; |
| import { PlotSimpleSkTraceEventDetails } from '../plot-simple-sk/plot-simple-sk'; |
| import { PlotSimpleSk } from '../plot-simple-sk/plot-simple-sk'; |
| import { CommitDetailPanelSk } from '../commit-detail-panel-sk/commit-detail-panel-sk'; |
| import '../window/window'; |
| |
| /** Defines a func that takes a number and formats it as a string. */ |
| type Formatter = (n: number)=> string; |
| |
| /** |
| * Each type of step detection gives different meaning to the regression, |
| * stepSize, and least_squares values in the cluster summary, so we create a |
| * mapping to describe each of their labels and how the values should be |
| * formatted. |
| */ |
| interface LabelsAndFormatters { |
| regression: string; |
| stepSize: string; |
| lse: string; |
| regressionFormatter: Formatter; |
| stepSizeFormatter: Formatter; |
| lseFormatter: Formatter; |
| } |
| |
| // Oddly this seems to be the only way to retrieve the Intl locale for the |
| // browser. |
| const locale = Intl.NumberFormat().resolvedOptions().locale; |
| |
| // These are different number formatters used in labelsForStepDetection. |
| const percentFormatter = Intl.NumberFormat(locale, { style: 'percent', maximumSignificantDigits: 4 }).format; |
| const decimalFormatter = Intl.NumberFormat(locale, { style: 'decimal', maximumSignificantDigits: 4 }).format; |
| const emptyFormatter = () => ''; |
| |
| /** Map each StepDetection to the labels and formatters used for that type. */ |
| const labelsForStepDetection: Record<StepDetection, LabelsAndFormatters> = { |
| '': { |
| regression: 'Regression Factor:', |
| regressionFormatter: decimalFormatter, |
| stepSize: 'Step Size:', |
| stepSizeFormatter: decimalFormatter, |
| lse: 'Least Squares Error:', |
| lseFormatter: decimalFormatter, |
| }, |
| absolute: { |
| regression: 'Absolute Change:', |
| regressionFormatter: decimalFormatter, |
| stepSize: '', |
| stepSizeFormatter: emptyFormatter, |
| lse: '', |
| lseFormatter: emptyFormatter, |
| }, |
| const: { |
| regression: 'Constant Threshhold:', |
| regressionFormatter: decimalFormatter, |
| stepSize: '', |
| stepSizeFormatter: emptyFormatter, |
| lse: '', |
| lseFormatter: emptyFormatter, |
| }, |
| percent: { |
| regression: 'Percentage Change:', |
| regressionFormatter: percentFormatter, |
| stepSize: '', |
| stepSizeFormatter: emptyFormatter, |
| lse: '', |
| lseFormatter: emptyFormatter, |
| }, |
| cohen: { |
| regression: 'Standard Deviations:', |
| regressionFormatter: decimalFormatter, |
| stepSize: '', |
| stepSizeFormatter: emptyFormatter, |
| lse: '', |
| lseFormatter: emptyFormatter, |
| }, |
| mannwhitneyu: { |
| regression: 'p:', |
| regressionFormatter: percentFormatter, |
| stepSize: '', |
| stepSizeFormatter: emptyFormatter, |
| lse: 'U:', |
| lseFormatter: decimalFormatter, |
| }, |
| }; |
| |
| export interface ClusterSummary2SkTriagedEventDetail { |
| columnHeader: ColumnHeader; |
| triage: TriageStatus; |
| } |
| |
| export interface ClusterSummary2SkOpenKeysEventDetail { |
| shortcut: string; |
| begin: number; |
| end: number; |
| xbar: ColumnHeader; |
| } |
| |
| export class ClusterSummary2Sk extends ElementSk { |
| private summary: ClusterSummary; |
| |
| private triageStatus: TriageStatus; |
| |
| private wordCloud: CollapseSk | null = null; |
| |
| private status: HTMLDivElement | null = null; |
| |
| private graph: PlotSimpleSk | null = null; |
| |
| private rangelink: HTMLAnchorElement | null = null; |
| |
| private commits: CommitDetailPanelSk | null = null; |
| |
| private frame: FrameResponse | null = null; |
| |
| private fullSummary: FullSummary | null = null; |
| |
| private _alert: Alert | null = null; |
| |
| private labels: LabelsAndFormatters = labelsForStepDetection[''] |
| |
| constructor() { |
| super(ClusterSummary2Sk.template); |
| this.summary = { |
| centroid: null, |
| shortcut: '', |
| step_point: null, |
| num: 0, |
| step_fit: { |
| regression: 0, |
| least_squares: 0, |
| step_size: 0, |
| turning_point: 0, |
| status: 'Uninteresting', |
| }, |
| param_summaries2: [], |
| ts: new Date().toISOString(), |
| }; |
| this.triageStatus = { |
| status: '', |
| message: '', |
| }; |
| } |
| |
| /** |
| * Look up the commit ids for the given offsets and sources. |
| * |
| * @param An array of CommitNumbers. |
| * @returns A Promise that resolves the cids and returns an Array of serialized perfgit.Commit. |
| */ |
| static lookupCids(cids: CommitNumber[]): Promise<Commit[]> { |
| return fetch('/_/cid/', { |
| method: 'POST', |
| body: JSON.stringify(cids), |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| }).then(jsonOrThrow); |
| } |
| |
| private static template = (ele: ClusterSummary2Sk) => html` |
| <div class="regression ${ele.statusClass()}"> |
| ${ele.labels.regression} |
| <span>${ele.labels.regressionFormatter(ele.summary!.step_fit!.regression)}</span> |
| </div> |
| <div class="stats"> |
| <div class="labelled"> |
| Cluster Size: |
| <span>${ele.summary.num}</span> |
| </div> |
| ${ClusterSummary2Sk.leastSquares(ele)} |
| <div class="labelled"> |
| ${ele.labels.stepSize} |
| <span>${ele.labels.stepSizeFormatter(ele.summary!.step_fit!.step_size)}</span> |
| </div> |
| </div> |
| <plot-simple-sk |
| class="plot" |
| width="800" |
| height="250" |
| specialevents |
| @trace_selected=${ele.traceSelected} |
| ></plot-simple-sk> |
| <div id="status" class=${ele.hiddenClass()}> |
| <p class="disabledMessage">You must be logged in to change the status.</p> |
| <triage2-sk |
| value=${ele.triageStatus.status} |
| @change=${(e: CustomEvent<Status>) => { |
| ele.triageStatus.status = e.detail; |
| }} |
| ></triage2-sk> |
| <input |
| type="text" |
| .value=${ele.triageStatus.message} |
| @change=${(e: InputEvent) => { |
| ele.triageStatus.message = (e.currentTarget! as HTMLInputElement).value; |
| }} |
| label="Message" |
| /> |
| <button class="action" @click=${ele.update}>Update</button> |
| </div> |
| <commit-detail-panel-sk id="commits" selectable></commit-detail-panel-sk> |
| <div class="actions"> |
| <button id="shortcut" @click=${ele.openShortcut}> |
| View on dashboard |
| </button> |
| <button @click=${ele.toggleWordCloud}>Word Cloud</button> |
| <a id="permalink" class=${ele.hiddenClass()} href=${ele.permaLink()}> |
| Permlink |
| </a> |
| <a id="rangelink" href="" target="_blank"></a> |
| </div> |
| <collapse-sk class="wordCloudCollapse" closed> |
| <word-cloud-sk .items=${ele.summary.param_summaries2}></word-cloud-sk> |
| </collapse-sk> |
| `; |
| |
| private static leastSquares = (ele: ClusterSummary2Sk) => html` |
| <div class="labelled"> |
| ${ele.labels.lse} |
| <span>${ele.labels.lseFormatter(ele.summary!.step_fit!.least_squares)}</span> |
| </div> |
| `; |
| |
| connectedCallback(): void { |
| super.connectedCallback(); |
| this._upgradeProperty('full_summary'); |
| this._upgradeProperty('triage'); |
| this._upgradeProperty('alert'); |
| this._render(); |
| this.wordCloud = this.querySelector('.wordCloudCollapse'); |
| this.status = this.querySelector('#status'); |
| this.graph = this.querySelector('plot-simple-sk'); |
| this.rangelink = this.querySelector('#rangelink'); |
| this.commits = this.querySelector('#commits'); |
| Login.then((status) => { |
| this.status!.classList.toggle('disabled', status.Email === ''); |
| }).catch(errorMessage); |
| |
| // eslint-disable-next-line no-self-assign |
| this.full_summary = this.full_summary; |
| // eslint-disable-next-line no-self-assign |
| this.triage = this.triage; |
| } |
| |
| private update() { |
| const columnHeader = this.summary.step_point!; |
| const detail: ClusterSummary2SkTriagedEventDetail = { |
| columnHeader, |
| triage: this.triage, |
| }; |
| this.dispatchEvent( |
| new CustomEvent<ClusterSummary2SkTriagedEventDetail>('triaged', { |
| detail, |
| bubbles: true, |
| }), |
| ); |
| } |
| |
| private openShortcut() { |
| const detail: ClusterSummary2SkOpenKeysEventDetail = { |
| shortcut: this.summary.shortcut, |
| begin: this.frame!.dataframe!.header![0]!.timestamp, |
| end: |
| this.frame!.dataframe!.header![ |
| this.frame!.dataframe!.header!.length - 1 |
| ]!.timestamp + 1, |
| xbar: this.summary.step_point!, |
| }; |
| this.dispatchEvent( |
| new CustomEvent<ClusterSummary2SkOpenKeysEventDetail>('open-keys', { |
| detail, |
| bubbles: true, |
| }), |
| ); |
| } |
| |
| private traceSelected(e: CustomEvent<PlotSimpleSkTraceEventDetails>) { |
| const commitNumber = this.frame!.dataframe!.header![e.detail.x]?.offset; |
| ClusterSummary2Sk.lookupCids([commitNumber!]) |
| .then((json) => { |
| this.commits!.details = json; |
| }) |
| .catch(errorMessage); |
| } |
| |
| private toggleWordCloud() { |
| this.wordCloud!.closed = !this.wordCloud!.closed; |
| } |
| |
| private hiddenClass() { |
| return this.hasAttribute('notriage') ? 'hidden' : ''; |
| } |
| |
| private permaLink() { |
| // Bounce to the triage page, but with the time range narrowed to |
| // contain just the step_point commit. |
| if (!this.summary || !this.summary.step_point) { |
| return ''; |
| } |
| const begin = this.summary.step_point.timestamp; |
| const end = begin + 1; |
| return `/t/?begin=${begin}&end=${end}&subset=all`; |
| } |
| |
| private statusClass() { |
| if (!this.summary) { |
| return ''; |
| } |
| const status = this.summary!.step_fit!.status || ''; |
| return status.toLowerCase(); |
| } |
| |
| /** @prop full_summary {string} A serialized: |
| * |
| * { |
| * summary: cluster2.ClusterSummary, |
| * frame: dataframe.FrameResponse, |
| * } |
| * |
| */ |
| get full_summary(): FullSummary | null { |
| return this.fullSummary; |
| } |
| |
| set full_summary(val: FullSummary | null) { |
| if (!val) { |
| return; |
| } |
| if (!val.frame) { |
| return; |
| } |
| this.fullSummary = val; |
| this.summary = val.summary; |
| this.frame = val.frame; |
| if (!this.graph) { |
| return; |
| } |
| |
| // Set the data- attributes used for sorting cluster summaries. |
| this.dataset.clustersize = this.summary.num.toString(); |
| this.dataset.steplse = this.summary!.step_fit!.least_squares.toPrecision(2); |
| this.dataset.stepsize = this.summary!.step_fit!.step_size.toPrecision(2); |
| this.dataset.stepregression = this.summary!.step_fit!.regression.toPrecision( |
| 2, |
| ); |
| // We take in a ClusterSummary, but need to transform all that data |
| // into a format that plot-sk can handle. |
| this.graph.removeAll(); |
| const labels: Date[] = []; |
| this.full_summary!.frame!.dataframe!.header!.forEach((header) => { |
| labels.push(new Date(header!.timestamp * 1000)); |
| }); |
| this.graph.addLines({ centroid: this.summary.centroid! }, labels); |
| // Set the x-bar but only if status != uninteresting. |
| if (this.summary!.step_fit!.status !== 'Uninteresting') { |
| // Loop through the dataframe header to find the location we should |
| // place the x-bar at. |
| const step = this.summary.step_point; |
| let xbar = -1; |
| this.frame!.dataframe!.header!.forEach((h, i) => { |
| if (h!.offset === step!.offset) { |
| xbar = i; |
| } |
| }); |
| if (xbar !== -1) { |
| this.graph.xbar = xbar; |
| } |
| |
| // If step_point is set then display the commit |
| // details for the xbar location. |
| if (step && step.offset > 0) { |
| ClusterSummary2Sk.lookupCids([step.offset]) |
| .then((json: Commit[]) => { |
| this.commits!.details = json; |
| }) |
| .catch(errorMessage); |
| } |
| |
| // Populate rangelink. |
| if (window.sk.perf.commit_range_url !== '') { |
| // First find the commit at step_fit, and the next previous commit that has data. |
| let prevCommit = xbar - 1; |
| while (prevCommit > 0 && this.summary!.centroid![prevCommit] === 1e32) { |
| prevCommit -= 1; |
| } |
| const cids: CommitNumber[] = [ |
| this.frame!.dataframe!.header![prevCommit]!.offset, |
| this.frame!.dataframe!.header![xbar]!.offset, |
| ]; |
| // Run those through cid lookup to get the hashes. |
| ClusterSummary2Sk.lookupCids(cids) |
| .then((json) => { |
| // Create the URL. |
| let url = window.sk.perf.commit_range_url; |
| url = url.replace('{begin}', json[0].hash); |
| url = url.replace('{end}', json[1].hash); |
| // Now populate link, including text and href. |
| this.rangelink!.href = url; |
| this.rangelink!.innerText = 'Commits At Step'; |
| }) |
| .catch(errorMessage); |
| } else { |
| this.rangelink!.href = ''; |
| this.rangelink!.innerText = ''; |
| } |
| } else { |
| this.rangelink!.href = ''; |
| this.rangelink!.innerText = ''; |
| } |
| |
| this._render(); |
| } |
| |
| /** @prop triage {string} The triage status of the cluster. |
| * Something of the form: |
| * |
| * { |
| * status: 'untriaged', |
| * message: 'This is a regression.', |
| * } |
| * |
| */ |
| get triage(): TriageStatus { |
| return this.triageStatus; |
| } |
| |
| set triage(val: TriageStatus) { |
| if (!val) { |
| return; |
| } |
| this.triageStatus = val; |
| this._render(); |
| } |
| |
| /** The configured Alert that found this regression. */ |
| get alert(): Alert | null { |
| return this._alert; |
| } |
| |
| set alert(val: Alert | null) { |
| if (!val) { |
| return; |
| } |
| this._alert = val; |
| this.labels = labelsForStepDetection[val!.step]; |
| this._render(); |
| } |
| } |
| |
| define('cluster-summary2-sk', ClusterSummary2Sk); |