Files
rosenpass/benchmarks/index.html
2025-06-03 10:45:16 +00:00

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>