mirror of
https://github.com/rosenpass/rosenpass.git
synced 2025-12-25 04:17:06 -08:00
877 lines
38 KiB
HTML
877 lines
38 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes" />
|
|
<style>
|
|
html {
|
|
font-family: BlinkMacSystemFont, -apple-system, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
|
'Fira Sans', 'Droid Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
-webkit-font-smoothing: antialiased;
|
|
background-color: #fff;
|
|
font-size: 16px;
|
|
}
|
|
body {
|
|
color: #4a4a4a;
|
|
margin: 8px;
|
|
font-size: 1em;
|
|
font-weight: 400;
|
|
}
|
|
header {
|
|
margin-bottom: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
main {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
a {
|
|
color: #3273dc;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
}
|
|
a:hover {
|
|
color: #000;
|
|
}
|
|
button {
|
|
color: #fff;
|
|
background-color: #3298dc;
|
|
border-color: transparent;
|
|
cursor: pointer;
|
|
text-align: center;
|
|
}
|
|
button:hover {
|
|
background-color: #2793da;
|
|
flex: none;
|
|
}
|
|
.spacer {
|
|
flex: auto;
|
|
}
|
|
.small {
|
|
font-size: 0.75rem;
|
|
}
|
|
footer {
|
|
margin-top: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.header-label {
|
|
margin-right: 4px;
|
|
}
|
|
.dropdowns {
|
|
display: flex;
|
|
gap: 25px;
|
|
}
|
|
.filter-interface {
|
|
display: flex;
|
|
gap: 25px;
|
|
}
|
|
.checklist {
|
|
padding-right: 10px;
|
|
}
|
|
.checklist-title {
|
|
font-weight: bold;
|
|
text-align: center;
|
|
}
|
|
.select-all {
|
|
background: grey;
|
|
color: white;
|
|
margin-right: 2px;
|
|
width: 100%;
|
|
}
|
|
.benchmark-set {
|
|
margin: 8px 0;
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.benchmark-title {
|
|
font-size: 3rem;
|
|
font-weight: 600;
|
|
word-break: break-word;
|
|
text-align: center;
|
|
}
|
|
.benchmark-graphs {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-around;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
width: 100%;
|
|
}
|
|
</style>
|
|
<title>Benchmarks</title>
|
|
</head>
|
|
|
|
<body>
|
|
<header id="header">
|
|
<div class="header-item">
|
|
<strong class="header-label">Last Update:</strong>
|
|
<span id="last-update"></span>
|
|
</div>
|
|
<div class="header-item">
|
|
<strong class="header-label">Repository:</strong>
|
|
<a id="repository-link" rel="noopener"></a>
|
|
</div>
|
|
</header>
|
|
<main id="main">
|
|
<div class="dropdowns">
|
|
<div id="ref-pr-dropdown">
|
|
<label id="ref-pr-dropdown-label" for="ref-pr-dropdown-select"></label>
|
|
<select id="ref-pr-dropdown-select"></select>
|
|
<div id="pr-url" style="display: inline; visibility: hidden">
|
|
[
|
|
<a id="pr-url-a">github</a>
|
|
]
|
|
</div>
|
|
</div>
|
|
<div id="benchmark-set-dropdown"></div>
|
|
<strong> Number of charts displayed: </strong>
|
|
<div id="num-charts-shown"></div>
|
|
<div id="clear-filters-btn-div"></div>
|
|
</div>
|
|
<div id="body"></div>
|
|
</main>
|
|
<footer></footer>
|
|
|
|
<script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"></script>
|
|
<script id="main-script">
|
|
'use strict';
|
|
(async function () {
|
|
function timeZoneOffset() {
|
|
// extract GMT+offset from current time zone, if available
|
|
return new Date()
|
|
.toString()
|
|
.split(' ')
|
|
.filter((s) => s.includes('GMT'))
|
|
.map((s) => `(${s})`);
|
|
}
|
|
|
|
function buildPlotLayout() {
|
|
const offset = timeZoneOffset();
|
|
const layout = {
|
|
height: 600,
|
|
width: 1200,
|
|
xaxis: { title: { text: `Time of benchmark run ${offset}` }, type: 'date' },
|
|
};
|
|
|
|
return layout;
|
|
}
|
|
|
|
function addTitleToElement(parent, title) {
|
|
const titleElem = document.createElement('h1');
|
|
titleElem.className = 'benchmark-title';
|
|
titleElem.textContent = title;
|
|
parent.appendChild(titleElem);
|
|
}
|
|
function chartFilteredTraces(parent, title, filteredTraces) {
|
|
// return if no datasets
|
|
if (filteredTraces.length == 0) {
|
|
console.error(`No datasets found with ${title}`);
|
|
return;
|
|
}
|
|
|
|
// adjust data to have the appropriate unit
|
|
const unit = adjustDataAndUnit(filteredTraces);
|
|
|
|
// create elem for plot
|
|
const elem = document.createElement('div');
|
|
elem.className = 'benchmark-graphs';
|
|
parent.appendChild(elem);
|
|
|
|
const layout = buildPlotLayout();
|
|
|
|
// get the unit from the first item
|
|
// this is possible because there is at least one data point,
|
|
// and the unit is the same across all data points.
|
|
layout.yaxis = { title: { text: `Value (${unit})` } };
|
|
|
|
layout.title = { text: title };
|
|
|
|
// add the plot to the elem
|
|
Plotly.newPlot(elem, filteredTraces, layout);
|
|
|
|
// return the element
|
|
return elem;
|
|
}
|
|
|
|
function buildKey(benchItem, schema) {
|
|
// build the key from the values in the schema
|
|
let key = {};
|
|
for (const s of schema) {
|
|
let value = benchItem[s];
|
|
if (typeof value === 'number') {
|
|
value = value.toString();
|
|
}
|
|
key[s] = value;
|
|
}
|
|
|
|
return JSON.stringify(key);
|
|
}
|
|
function parseKey(keyString, schema) {
|
|
const key = JSON.parse(keyString);
|
|
for (const s of schema) {
|
|
if (!key.hasOwnProperty(s)) {
|
|
// explicitly set to `undefined`
|
|
key[s] = undefined;
|
|
}
|
|
}
|
|
return key;
|
|
}
|
|
function getNanoseconds(value, unit) {
|
|
// XXX: should unify unit format in action output
|
|
// XXX: assumes duration
|
|
const unitIdentifier = unit[0];
|
|
|
|
let newValue;
|
|
switch (unitIdentifier) {
|
|
// micro
|
|
case '\u03bc':
|
|
case '\u00b5':
|
|
case 'u':
|
|
newValue = value * 1000;
|
|
break;
|
|
case 'n':
|
|
newValue = value;
|
|
break;
|
|
case 'm':
|
|
newValue = value * 1000000;
|
|
break;
|
|
case 's':
|
|
newValue = value * 1000000000;
|
|
break;
|
|
default:
|
|
throw new Error(`undefined unit: ${unitIdentifier}`);
|
|
}
|
|
return newValue;
|
|
}
|
|
// adjust all data to same unit and get optimal unit
|
|
// returns the unit, and adjusts the traces in place
|
|
function adjustDataAndUnit(traces) {
|
|
// get the max value across all traces
|
|
const maxes = traces.map((trace) => Math.max(...trace.dataNs));
|
|
const maxValue = Math.max(...maxes);
|
|
|
|
// determine best unit
|
|
let scaleFactor;
|
|
let unit;
|
|
if (maxValue > 1000000000) {
|
|
scaleFactor = 1000000000.0;
|
|
unit = 's/iter';
|
|
} else if (maxValue > 1000000) {
|
|
scaleFactor = 1000000.0;
|
|
unit = 'ms/iter';
|
|
} else if (maxValue > 1000) {
|
|
scaleFactor = 1000.0;
|
|
unit = '\u03BCs/iter';
|
|
} else {
|
|
scaleFactor = 1.0;
|
|
unit = 'ns/iter';
|
|
}
|
|
// add the correct y axis data
|
|
traces.forEach((trace) => {
|
|
trace.y = trace.dataNs.map((d) => d / scaleFactor);
|
|
});
|
|
|
|
return unit;
|
|
}
|
|
// separates the data into traces by key
|
|
function separateAllTraces(commits, schema) {
|
|
// flatten and keep commit data
|
|
const data = commits
|
|
.map((commitEntry) => {
|
|
const { commit, date, benches } = commitEntry;
|
|
return benches.map((bench) => {
|
|
return { commit, date, bench };
|
|
});
|
|
})
|
|
.flat();
|
|
|
|
// group by key
|
|
const groupedData = Object.groupBy(data, (benchEntry) => {
|
|
return buildKey(benchEntry.bench, schema);
|
|
});
|
|
|
|
// prepare for plotting
|
|
const traces = Object.entries(groupedData).map(([keyString, dataset]) => {
|
|
const metadata = parseKey(keyString, schema);
|
|
// first convert to nanoseconds
|
|
const dataNs = dataset.map((d) => getNanoseconds(d.bench.value, d.bench.unit));
|
|
metadata.unit = 'ns/iter';
|
|
|
|
return {
|
|
metadata,
|
|
x: dataset.map((d) => new Date(d.date)),
|
|
// y will be constructed later
|
|
dataNs,
|
|
dataset,
|
|
showlegend: true,
|
|
hoverinfo: 'text',
|
|
};
|
|
});
|
|
|
|
return Array.from(traces);
|
|
}
|
|
// get the correct schema object from `window.BENCHMARK_DATA`,
|
|
// and return a default value if invalid or none is provided.
|
|
function retrieveSchema(benchSet) {
|
|
const defaultSchema = ['name', 'platform', 'os', 'keySize', 'api', 'category'];
|
|
|
|
let schema = window.BENCHMARK_DATA.schema;
|
|
if (!schema || typeof schema !== 'object' || !schema.hasOwnProperty(benchSet)) {
|
|
console.error(`No or invalid schema provided: defaulting to [${defaultSchema}]`);
|
|
return defaultSchema;
|
|
}
|
|
|
|
return schema[benchSet];
|
|
}
|
|
// get the correct groupBy object from `window.BENCHMARK_DATA`,
|
|
// and return a default value if invalid or none is provided.
|
|
function retrieveGroupBy(benchSet) {
|
|
const defaultGroupBy = ['os'];
|
|
let groupBy = window.BENCHMARK_DATA.groupBy;
|
|
if (!groupBy || typeof groupBy !== 'object' || !groupBy.hasOwnProperty(benchSet)) {
|
|
console.error(`No or invalid groupBy provided: defaulting to [${defaultGroupBy}]`);
|
|
return defaultGroupBy;
|
|
}
|
|
return groupBy[benchSet];
|
|
}
|
|
|
|
// build the groupKey for a trace,
|
|
// which consists of the key-value pairs
|
|
// for the groupBy keys only.
|
|
function buildTraceGroupKey(trace, groupBy) {
|
|
const traceGroup = {};
|
|
for (let key of groupBy) {
|
|
let value = trace.metadata[key];
|
|
if (typeof value === 'number') {
|
|
value = value.toString();
|
|
}
|
|
traceGroup[key] = value;
|
|
}
|
|
return JSON.stringify(traceGroup);
|
|
}
|
|
function getObservationTooltipText(name, observation) {
|
|
const value = observation.bench.value;
|
|
const unit = observation.bench.unit;
|
|
const range = observation.bench.range;
|
|
const commitId = observation.commit.id;
|
|
const message = observation.commit.message;
|
|
const url = observation.commit.url;
|
|
const rangeText = range ? range : '';
|
|
|
|
return `<b>${name}</b><br>value: ${value} ${unit} ${rangeText}<br>commit id: ${commitId}<br>commit name: ${message}<br>commit url: ${url}`;
|
|
}
|
|
// return the name for the graph legend,
|
|
// using only the metadata entries that are not included in the
|
|
// groupBy, and are not 'unit'.
|
|
// also, don't include the fields whose values are `undefined` in the name.
|
|
function getLegendName(trace, groupBy, schema) {
|
|
// entries sorted by schema
|
|
const orderedEntries = schema.map((key) => [key, trace.metadata[key]]);
|
|
|
|
return orderedEntries
|
|
.filter(([key, value]) => !groupBy.includes(key) && key !== 'unit' && value !== undefined)
|
|
.map(([_, value]) => value)
|
|
.join(' ');
|
|
}
|
|
function addLegendNameAndTooltip(trace, groupBy, schema) {
|
|
// set the name in the legend
|
|
trace.name = getLegendName(trace, groupBy, schema);
|
|
|
|
// set the tooltip text
|
|
trace.text = trace.dataset.map((observation) => getObservationTooltipText(trace.name, observation));
|
|
}
|
|
|
|
// Display the fields in the group as a comma-separated list
|
|
function buildTitleFromGroupKey(groupKey, groupBy) {
|
|
const entries = groupBy
|
|
.map((field) => [field, groupKey[field]])
|
|
.map(([field, value]) => {
|
|
if (value === undefined) {
|
|
return `undefined ${field}`;
|
|
}
|
|
return `the ${field} ${value}`;
|
|
});
|
|
|
|
if (entries.length === 0) {
|
|
return 'Results';
|
|
}
|
|
if (entries.length === 1) {
|
|
return 'Results for run with ' + entries[0];
|
|
}
|
|
|
|
let joinedEntries = '';
|
|
joinedEntries += entries.slice(0, entries.length - 1).join(', ');
|
|
joinedEntries += ' and ' + entries[entries.length - 1];
|
|
return 'Results for run with ' + joinedEntries;
|
|
}
|
|
function addAttributesToChartElement(elem, groupKey) {
|
|
Object.entries(groupKey).forEach(([key, value]) => {
|
|
elem.setAttribute(key.replace(' ', ''), value);
|
|
});
|
|
}
|
|
function updateSelectAllStatus(add, selectAll, max) {
|
|
let numSelected = selectAll.getAttribute('num-selected');
|
|
numSelected = numSelected ? Number(numSelected) : 0;
|
|
numSelected = numSelected ? numSelected : 0;
|
|
|
|
if (add) {
|
|
numSelected += 1;
|
|
} else if (!add && numSelected === 0) {
|
|
numSelected = 0;
|
|
} else {
|
|
numSelected -= 1;
|
|
}
|
|
|
|
selectAll.setAttribute('num-selected', numSelected);
|
|
|
|
// set the state
|
|
if (numSelected === 0) {
|
|
selectAll.checked = false;
|
|
selectAll.indeterminate = false;
|
|
} else if (numSelected === max) {
|
|
selectAll.checked = true;
|
|
selectAll.indeterminate = false;
|
|
} else {
|
|
selectAll.checked = false;
|
|
selectAll.indeterminate = true;
|
|
}
|
|
}
|
|
|
|
function updateNumShown(numShown) {
|
|
const elem = document.getElementById('num-charts-shown');
|
|
elem.textContent = numShown;
|
|
}
|
|
|
|
function setChartHiddenStatus(hide, key, value) {
|
|
value = value ? value : 'undefined';
|
|
value = typeof value === 'number' ? value.toString() : value;
|
|
const charts = document.getElementsByClassName('benchmark-graphs');
|
|
const filteredCharts = Array.from(charts).filter((chart) => {
|
|
const chartValue = chart.getAttribute(key.replace(' ', ''));
|
|
return chartValue === value;
|
|
});
|
|
filteredCharts.forEach((chart) => {
|
|
let hiddenByAttribute = chart.getAttribute('hidden-by');
|
|
let hiddenBy = hiddenByAttribute ? Number(hiddenByAttribute) : 0;
|
|
hiddenBy = hiddenBy ? hiddenBy : 0;
|
|
|
|
if (hide) {
|
|
chart.style.setProperty('display', 'none');
|
|
hiddenBy += 1;
|
|
} else {
|
|
if (hiddenBy) {
|
|
hiddenBy -= 1;
|
|
} else {
|
|
hiddenBy = 0;
|
|
}
|
|
|
|
// only unhide if not hidden by another
|
|
if (hiddenBy == 0) {
|
|
chart.style.setProperty('display', null);
|
|
}
|
|
}
|
|
chart.setAttribute('hidden-by', hiddenBy);
|
|
});
|
|
|
|
// update number of charts hidden
|
|
const numShown = Array.from(charts).filter((chart) => {
|
|
return chart.style.getPropertyValue('display') !== 'none';
|
|
}).length;
|
|
|
|
updateNumShown(numShown);
|
|
}
|
|
|
|
// get the unique values for each group
|
|
function getUniqueValuesByKey(groupKeys, groupBy) {
|
|
const uniqueValues = {};
|
|
groupBy.forEach((key) => {
|
|
uniqueValues[key] = [...new Set(groupKeys.map((k) => k[key]))];
|
|
});
|
|
|
|
return uniqueValues;
|
|
}
|
|
function createFilterInterface(elem, uniqueValues) {
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
// button to clear all filters
|
|
const clearFilters = document.getElementById('clear-filters-btn-div');
|
|
clearFilters.innerHTML = '';
|
|
const btn = document.createElement('button');
|
|
btn.setAttribute('id', 'clear-filters-btn');
|
|
btn.textContent = 'Clear all filters';
|
|
btn.disabled = true;
|
|
btn.addEventListener('click', function () {
|
|
Object.entries(uniqueValues).forEach(([key, values]) => {
|
|
values.forEach((value) => {
|
|
const checkbox = document.getElementById(`${key}-${value}`);
|
|
checkbox.checked = true;
|
|
const hidden = false;
|
|
|
|
// hide the chart
|
|
setChartHiddenStatus(hidden, key, value);
|
|
});
|
|
const numSelected = values.length;
|
|
const selectAll = document.getElementById(`${key}-select-all`);
|
|
selectAll.setAttribute('num-selected', numSelected);
|
|
selectAll.checked = true;
|
|
selectAll.indeterminate = false;
|
|
});
|
|
});
|
|
clearFilters.appendChild(btn);
|
|
|
|
Object.entries(uniqueValues).forEach(([key, values]) => {
|
|
const checklist = document.createElement('div');
|
|
checklist.className = 'checklist';
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'checklist-title';
|
|
title.textContent = key;
|
|
checklist.appendChild(title);
|
|
|
|
// select all / deselect all
|
|
const selectAllWrapper = document.createElement('div');
|
|
selectAllWrapper.className = 'select-all';
|
|
|
|
const selectAll = document.createElement('input');
|
|
selectAll.setAttribute('id', `${key}-select-all`);
|
|
selectAll.setAttribute('type', 'checkbox');
|
|
const checkboxId = `${key}-select-all-checkbox`;
|
|
const selectAllLabel = document.createElement('label');
|
|
selectAllLabel.setAttribute('for', checkboxId);
|
|
selectAllLabel.textContent = 'select all';
|
|
selectAllWrapper.appendChild(selectAll);
|
|
selectAllWrapper.appendChild(selectAllLabel);
|
|
checklist.appendChild(selectAllWrapper);
|
|
|
|
selectAll.addEventListener('change', function () {
|
|
values.forEach((value) => {
|
|
const checkbox = document.getElementById(`${key}-${value}`);
|
|
checkbox.checked = this.checked;
|
|
const hidden = !this.checked;
|
|
|
|
// hide the chart
|
|
setChartHiddenStatus(hidden, key, value);
|
|
|
|
// enable clear filters button
|
|
if (hidden) {
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
const numSelected = this.checked ? values.length : 0;
|
|
selectAll.setAttribute('num-selected', numSelected);
|
|
});
|
|
|
|
values.forEach((value) => {
|
|
// select all
|
|
updateSelectAllStatus(true, selectAll, values.length);
|
|
|
|
// generate the filter interface from the unique values
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'checklist-entry';
|
|
|
|
// id to match checkbox to label
|
|
const id = `${key}-${value}`;
|
|
|
|
// single checkbox with label
|
|
const checkBox = document.createElement('input');
|
|
checkBox.setAttribute('type', 'checkbox');
|
|
checkBox.setAttribute('id', id);
|
|
|
|
// start with checkbox checked
|
|
checkBox.checked = true;
|
|
|
|
// set the key and value attributes
|
|
checkBox.setAttribute('key', key);
|
|
checkBox.setAttribute('value', value);
|
|
|
|
// create the label for the checkbox
|
|
const label = document.createElement('label');
|
|
label.setAttribute('for', id);
|
|
label.textContent = value ? value : 'undefined';
|
|
|
|
checkBox.addEventListener('change', function () {
|
|
const key = this.getAttribute('key');
|
|
const value = this.getAttribute('value');
|
|
setChartHiddenStatus(!this.checked, key, value);
|
|
updateSelectAllStatus(this.checked, selectAll, values.length);
|
|
});
|
|
wrapper.appendChild(checkBox);
|
|
wrapper.appendChild(label);
|
|
|
|
checklist.appendChild(wrapper);
|
|
});
|
|
|
|
fragment.appendChild(checklist);
|
|
});
|
|
elem.appendChild(fragment);
|
|
}
|
|
function populateRefPrDropdown(refsOpt, prsOpt) {
|
|
const refs = refsOpt ? refsOpt : [];
|
|
const prs = prsOpt ? prsOpt : [];
|
|
|
|
// if there are no refs or prs, remove the dropdown
|
|
if (refs.length == 0 && prs.length == 0) {
|
|
return;
|
|
}
|
|
const label = document.getElementById('ref-pr-dropdown-label');
|
|
|
|
const select = document.getElementById('ref-pr-dropdown-select');
|
|
|
|
function getRefText(ref) {
|
|
const data = ref.split('/');
|
|
const type = data[1] ? data[1] : '';
|
|
let typeText;
|
|
if (type === 'heads') {
|
|
typeText = 'branch ' + ref.replace('refs/heads/', '');
|
|
} else if (type === 'tags') {
|
|
typeText = 'tag' + ref.replace('refs/tags/', '');
|
|
} else {
|
|
// fallback
|
|
typeText = ref;
|
|
}
|
|
return typeText;
|
|
}
|
|
|
|
refs.forEach((name) => {
|
|
const option = document.createElement('option');
|
|
const attribute = `ref ${name}`;
|
|
option.setAttribute('value', attribute);
|
|
option.textContent = getRefText(name);
|
|
|
|
select.appendChild(option);
|
|
});
|
|
prs.forEach((name) => {
|
|
const option = document.createElement('option');
|
|
const text = `pr ${name}`;
|
|
option.setAttribute('value', text);
|
|
option.textContent = text;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
select.addEventListener('change', async function () {
|
|
const [type, value] = this.value.split(' ');
|
|
if (type === 'pr') {
|
|
await loadDataFromPr(value);
|
|
} else {
|
|
await loadDataFromRef(value);
|
|
}
|
|
rerenderAll();
|
|
});
|
|
}
|
|
function populateBenchSetDropdown(names) {
|
|
let elem = document.getElementById('benchmark-set-dropdown');
|
|
elem.innerHTML = '';
|
|
|
|
// if there is one or fewer names, remove the dropdown
|
|
if (names.length === 0) {
|
|
return;
|
|
}
|
|
const label = document.createElement('label');
|
|
label.setAttribute('for', 'bench-set-dropdown');
|
|
elem.appendChild(label);
|
|
|
|
const select = document.createElement('select');
|
|
select.id = 'bench-set-dropdown';
|
|
elem.appendChild(select);
|
|
|
|
names.forEach((name) => {
|
|
const option = document.createElement('option');
|
|
option.setAttribute('value', name);
|
|
option.textContent = name;
|
|
select.appendChild(option);
|
|
});
|
|
select.addEventListener('change', async function () {
|
|
const benchSet = this.value;
|
|
renderAllCharts(benchSet);
|
|
});
|
|
}
|
|
function renderAllCharts(benchSet) {
|
|
const main = document.getElementById('body');
|
|
main.innerHTML = '';
|
|
|
|
// retrieve the data
|
|
const entry = window.BENCHMARK_DATA.entries[benchSet];
|
|
|
|
// retrieve the custom metadata schema from `window.BENCHMARK_DATA`
|
|
// each combination of fields is used to uniquely identify a trace
|
|
const schema = retrieveSchema(benchSet);
|
|
|
|
// build the data traces by separating out the observations
|
|
// by key. This is equivalent to separating out the observations
|
|
// by benchmark id, except that the benchmark id consists
|
|
// of multiple, separate fields.
|
|
const traces = separateAllTraces(entry, schema);
|
|
|
|
// get the groupBy information from `window.BENCHMARK_DATA`
|
|
// this is an array of keys, e.g. ['os', 'keySize']
|
|
// there should be one plot per combination of these values
|
|
const groupBy = retrieveGroupBy(benchSet);
|
|
|
|
// create a div for the filter interface
|
|
const filterElem = document.createElement('div');
|
|
filterElem.className = 'filter-interface';
|
|
|
|
// create a div for the benchmark set
|
|
const setElem = document.createElement('div');
|
|
setElem.className = 'benchmark-set';
|
|
addTitleToElement(setElem, `${benchSet} by ${groupBy}`);
|
|
|
|
// group datasets by the relevant keys
|
|
const groupedData = Object.groupBy(traces, (trace) => buildTraceGroupKey(trace, groupBy));
|
|
const groupKeys = Object.keys(groupedData).map((keyString) => parseKey(keyString, groupBy));
|
|
|
|
// create the interface at the top of the page for filtering
|
|
const uniqueValues = getUniqueValuesByKey(groupKeys, groupBy);
|
|
createFilterInterface(filterElem, uniqueValues);
|
|
|
|
let numShown = 0;
|
|
// create a chart for each group
|
|
Object.entries(groupedData).forEach(([groupKeyString, filteredTraces]) => {
|
|
// build the title
|
|
const groupKey = parseKey(groupKeyString, groupBy);
|
|
const title = buildTitleFromGroupKey(groupKey, groupBy);
|
|
|
|
// add the legend name to each trace,
|
|
// as well as the tooltip text for each point in each trace
|
|
filteredTraces.forEach((trace) => addLegendNameAndTooltip(trace, groupBy, schema));
|
|
|
|
const chartElem = chartFilteredTraces(setElem, title, filteredTraces);
|
|
if (chartElem) {
|
|
addAttributesToChartElement(chartElem, groupKey);
|
|
numShown += 1;
|
|
}
|
|
});
|
|
|
|
main.appendChild(filterElem);
|
|
main.appendChild(setElem);
|
|
|
|
updateNumShown(numShown);
|
|
}
|
|
async function checkFileAvailable(url) {
|
|
const response = await fetch(url, { method: 'HEAD' }).catch((e) => {
|
|
throw new Error(`Error retrieving data from ${url}: ${e}`);
|
|
});
|
|
if (response.status !== 200) {
|
|
throw new Error(`Error retrieving data from ${url}: ${response.statusText}`);
|
|
}
|
|
}
|
|
async function loadDataFromUrl(url) {
|
|
const response = await fetch(url).catch((e) => {
|
|
throw new Error(`Error retrieving data from ${url}: ${e}`);
|
|
});
|
|
if (response.status !== 200) {
|
|
throw new Error(`Error retrieving data from ${url}: ${response.statusText}`);
|
|
}
|
|
try {
|
|
return response.json();
|
|
} catch (e) {
|
|
throw new Error(`Error parsing JSON from ${url}: ${e}`);
|
|
}
|
|
}
|
|
async function loadDataFromRef(ref) {
|
|
const dataUrl = `${ref}.json`;
|
|
window.BENCHMARK_DATA = await loadDataFromUrl(dataUrl);
|
|
|
|
const elem = document.getElementById('pr-url');
|
|
elem.style.visibility = 'visible';
|
|
|
|
const a = document.getElementById('pr-url-a');
|
|
const repoUrl = window.BENCHMARK_DATA.repoUrl;
|
|
a.href = `${repoUrl}/tree/${ref}`;
|
|
|
|
return window.BENCHMARK_DATA;
|
|
}
|
|
async function loadDataFromPr(pr) {
|
|
const dataUrl = `pr/${pr}.json`;
|
|
window.BENCHMARK_DATA = await loadDataFromUrl(dataUrl);
|
|
|
|
// set the link to the PR
|
|
const elem = document.getElementById('pr-url');
|
|
elem.style.visibility = 'visible';
|
|
|
|
const a = document.getElementById('pr-url-a');
|
|
const repoUrl = window.BENCHMARK_DATA.repoUrl;
|
|
a.href = `${repoUrl}/pull/${pr}`;
|
|
|
|
return window.BENCHMARK_DATA;
|
|
}
|
|
function rerenderAll() {
|
|
// retrieve the data for the benchmark set with the first name
|
|
const benchSets = window.BENCHMARK_DATA.entries;
|
|
const [benchSet, entry] = Object.entries(benchSets)[0];
|
|
|
|
// build the dropdown for all benchmark sets
|
|
populateBenchSetDropdown(Object.keys(benchSets));
|
|
renderAllCharts(benchSet);
|
|
}
|
|
async function filterListing(listing) {
|
|
if (listing.refs) {
|
|
const promises = listing.refs.map(async (ref) => {
|
|
let keep = true;
|
|
try {
|
|
await checkFileAvailable(`${ref}.json`);
|
|
} catch (e) {
|
|
console.error(`Skipping ref in listing: ${e}`);
|
|
keep = false;
|
|
}
|
|
return { keep, ref };
|
|
});
|
|
|
|
listing.refs = (await Promise.all(promises))
|
|
.filter((result) => result.keep)
|
|
.map((result) => result.ref);
|
|
}
|
|
if (listing.prs) {
|
|
const promises = listing.prs.map(async (pr) => {
|
|
let keep = true;
|
|
try {
|
|
await checkFileAvailable(`pr/${pr}.json`);
|
|
} catch (e) {
|
|
console.error(`Skipping pr in listing: ${e}`);
|
|
keep = false;
|
|
}
|
|
return { keep, pr };
|
|
});
|
|
|
|
listing.prs = (await Promise.all(promises))
|
|
.filter((result) => result.keep)
|
|
.map((result) => result.pr);
|
|
}
|
|
}
|
|
async function loadFirstDataset(listing) {
|
|
if (listing.refs[0]) {
|
|
return await loadDataFromRef(listing.refs[0]);
|
|
}
|
|
if (listing.prs[0]) {
|
|
return await loadDataFromPr(listing.prs[0]);
|
|
}
|
|
throw new Error('listing contains no valid datasets');
|
|
}
|
|
|
|
async function init() {
|
|
const listing = await loadDataFromUrl('listing.json');
|
|
|
|
await filterListing(listing);
|
|
|
|
populateRefPrDropdown(listing.refs, listing.prs);
|
|
|
|
const data = await loadFirstDataset(listing);
|
|
// Render header
|
|
document.getElementById('last-update').textContent = new Date(data.lastUpdate).toString();
|
|
const repoLink = document.getElementById('repository-link');
|
|
repoLink.href = data.repoUrl;
|
|
repoLink.textContent = data.repoUrl;
|
|
|
|
rerenderAll();
|
|
}
|
|
|
|
await init();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|