blob: dba0a5313df25537fc8f73c3e9e691215d027ef8 [file] [log] [blame]
/**
* @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);