blob: c0ff613f364de5d0304d33dc7845a465c4afeae9 [file] [log] [blame]
/**
* @module modules/anomaly-sk
* @description <h2><code>anomaly-sk</code></h2>
*
*/
import { html, TemplateResult } from 'lit-html';
import { define } from '../../../elements-sk/modules/define';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import {
Anomaly,
AnomalyMap,
ColumnHeader,
CommitNumber,
TraceSet,
} from '../json';
import { AnomalyData } from '../plot-simple-sk/plot-simple-sk';
import { lookupCids } from '../cid/cid';
import '../window/window';
/**
* Use DataFrame and AnomalyMap to construct an AnomalyDataMap object. This
* object maps each trace to its anomalies and their coordinates. Constructing
* this object, makes it easier to plot the anomalies and display their
* metadata when clicked.
*
* Example Input:
* traceSet:
* {
* 'traceA': [142.3, 120.0, 120.0],
* 'traceB': [500.0, 632.2, 120.0],
* }
*
* header:
* [{offset: 1234, ...}, {offset: 1236, ...}, {offset: 1239}]
*
* anomalymap:
* {
* 'traceA': {'1234': a1},
* 'traceB': {'1234': a2, '1236': a3}
* }
*
* Example Output:
* {
* 'traceA': [{'x': 1, 'y': 142.3, 'anomaly': a1}],
* 'traceB': [{'x': 1, 'y': 500.0, 'anomaly': a2},
* {'x': 2, 'y': 623.2, 'anomaly': a3}]
* }
*
* @param {TraceSet} traceSet - A TraceSet object. We only look for anomalies
* from traces in traceSet. The traces also provide y coordinate positions.
* @param {Object} header - A ColumnHeader array. We use header to map commit positions
* to x coordinate positions.
* @param {Object} anomalymap - an AnomalyMap object. This object contains all
* the Anomaly objects used to populate the anomalyDataMap.
*/
export const getAnomalyDataMap = (
traceSet: TraceSet,
header: (ColumnHeader | null)[],
anomalymap: AnomalyMap
): { [key: string]: AnomalyData[] } => {
const anomalyDataMap: { [traceId: string]: AnomalyData[] } = {};
// Iterate the traceSet and see if a trace has anomalies in the anomaly map.
for (const traceId in traceSet) {
const trace = traceSet[traceId];
if (traceId in anomalymap!) {
const cidAnomalyMap = anomalymap![traceId];
if (cidAnomalyMap !== null) {
anomalyDataMap[traceId] = [];
// If it has anomalies, add all of them as AnomalyData objects.
// To find the y coord, search the commit number on columnHeader.
Object.keys(cidAnomalyMap)
.map(Number)
.forEach((cid) => {
for (let i = 0; i < header!.length; i++) {
const columnHeader = header![i];
// There are scenarios where a trace is missing due to upload failure,
// so we may not get a perfect match for the cid in the header offset.
// Simply use the next available commit in this case. This way if the
// anomaly is specified on a trace that failed uploading to skia perf,
// we show the anomaly on the next available data point instead of not
// displaying it at all.
if (columnHeader!.offset >= cid) {
anomalyDataMap[traceId].push({
anomaly: cidAnomalyMap[cid],
x: i,
y: trace[i],
});
break;
}
}
});
}
}
}
return anomalyDataMap;
};
const commitNumberToHashes = async (
cids: CommitNumber[]
): Promise<string[]> => {
const json = await lookupCids(cids);
return [json.commitSlice![0].hash, json.commitSlice![1].hash];
};
export class AnomalySk extends ElementSk {
private _anomaly: Anomaly | null = null;
private _bugHostUrl: string = 'https://bugs.chromium.org';
private _revision: TemplateResult = html``;
constructor() {
super(AnomalySk.template);
}
static formatNumber = (num: number): string =>
num.toLocaleString('en-US', {
maximumFractionDigits: 4,
});
static formatPercentage = (num: number): string =>
num.toLocaleString('en-US', {
maximumFractionDigits: 4,
signDisplay: 'exceptZero',
});
static getPercentChange = (
median_before: number,
median_after: number
): number => {
const difference = median_after - median_before;
// Division by zero is represented by infinity symbol.
return (100 * difference) / median_before;
};
formatRevisionRange = async (): Promise<void> => {
if (this.anomaly == null) return;
const start_rev = this.anomaly.start_revision;
const end_rev = this.anomaly.end_revision;
const cids: CommitNumber[] = [
CommitNumber(start_rev),
CommitNumber(end_rev),
];
const hashes = await commitNumberToHashes(cids);
let url = window.perf.commit_range_url;
if ([null, undefined, ''].includes(url)) {
this._revision = html`${start_rev} - ${end_rev}`;
return;
}
url = url.replace('{begin}', hashes[0]);
url = url.replace('{end}', hashes[1]);
this._revision = html`<a href="${url}" target=_blank>${start_rev} - ${end_rev}</td>`;
this._render();
};
static formatBug(bugHostUrl: string, bugId: number): TemplateResult {
if (bugId === -1) {
return html``;
}
return html`<a href="${`${bugHostUrl}/${bugId}`}" target=_blank>${bugId}</td>`;
}
private static template = (ele: AnomalySk) => {
if (ele._anomaly === null) {
return html``;
}
const anomaly = ele._anomaly!;
return html`
<div>
<table>
<thead>
<tr>
<th colspan="2">Anomaly Details</th>
</tr>
</thead>
<tbody>
<tr>
<th>Score</th>
<td>${AnomalySk.formatNumber(anomaly.median_after_anomaly)}</td>
</tr>
<tr>
<th>Prior Score</th>
<td>${AnomalySk.formatNumber(anomaly.median_before_anomaly)}</td>
</tr>
<tr>
<th>Percent Change</th>
<td>
${AnomalySk.formatPercentage(
AnomalySk.getPercentChange(
anomaly.median_before_anomaly,
anomaly.median_after_anomaly
)
)}%
</td>
</tr>
<tr>
<th>Revision Range</th>
<td>${ele.revision}</td>
</tr>
<tr>
<th>Improvement</th>
<td>${anomaly.is_improvement}</td>
</tr>
<tr>
<th>Bug Id</th>
<td>${AnomalySk.formatBug(ele.bugHostUrl, anomaly.bug_id)}</td>
</tr>
</tbody>
</table>
</div>
`;
};
connectedCallback(): void {
super.connectedCallback();
this._upgradeProperty('anomaly');
this._render();
}
get anomaly(): Anomaly | null {
return this._anomaly;
}
set anomaly(anomaly: Anomaly | null) {
this._anomaly = anomaly;
this.formatRevisionRange();
this._render();
}
get bugHostUrl(): string {
return this._bugHostUrl;
}
set bugHostUrl(url: string) {
if (url !== '') {
// Trim the trailing '/' since we are adding it in the format.
if (url.endsWith('/')) {
url = url.substring(0, url.length - 1);
}
this._bugHostUrl = url;
}
}
get revision(): TemplateResult {
return this._revision;
}
}
define('anomaly-sk', AnomalySk);