Skip to content

Commit 6f324ef

Browse files
authored
[Benchmarks] Add chart annotations (#19023)
with information on each version change of important dependecies, like Compute Runtime and benchmarks repos. To see a label with a new version of Compute Runtime or a benchmark, click the annotation.
1 parent d046c06 commit 6f324ef

File tree

3 files changed

+253
-23
lines changed

3 files changed

+253
-23
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// Part of the Unified-Runtime Project, under the Apache License v2.0 with LLVM Exceptions.
3+
// See LICENSE.TXT
4+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5+
6+
/**
7+
* Find version changes in data points to create annotations
8+
* @param {Array} points - Data points to analyze
9+
* @param {string} versionKey - Key to track version changes
10+
* @returns {Array} - List of change points
11+
*/
12+
function findVersionChanges(points, versionKey) {
13+
if (!points || points.length < 2) return [];
14+
15+
const changes = [];
16+
// Sort points by date
17+
const sortedPoints = [...points].sort((a, b) => a.x - b.x);
18+
let lastVersion = sortedPoints[0][versionKey];
19+
20+
for (let i = 1; i < sortedPoints.length; i++) {
21+
const currentPoint = sortedPoints[i];
22+
23+
const currentVersion = currentPoint[versionKey];
24+
if (currentVersion && currentVersion !== lastVersion) {
25+
changes.push({
26+
date: currentPoint.x,
27+
newVersion: currentVersion,
28+
});
29+
lastVersion = currentVersion;
30+
}
31+
}
32+
33+
return changes;
34+
}
35+
36+
/**
37+
* Add version change annotations to chart options
38+
* @param {Object} data - Chart data
39+
* @param {Object} options - Chart.js options object
40+
*/
41+
function addVersionChangeAnnotations(data, options) {
42+
const benchmarkSources = Array.from(window.annotationsOptions.values());
43+
const changeTrackers = [
44+
{
45+
// Benchmark repos updates
46+
versionKey: 'gitBenchHash',
47+
sources: benchmarkSources,
48+
pointsFilter: (points, url) => points.filter(p => p.gitBenchUrl === url),
49+
formatLabel: (sourceName, version) => `${sourceName}: ${version.substring(0, 7)}`
50+
},
51+
{
52+
// Compute Runtime updates
53+
versionKey: 'compute_runtime',
54+
sources: [
55+
{
56+
name: "Compute Runtime",
57+
url: "https://github.com/intel/compute-runtime.git",
58+
color: {
59+
border: 'rgba(70, 105, 150, 0.8)',
60+
background: 'rgba(70, 105, 150, 0.9)',
61+
},
62+
}
63+
],
64+
}
65+
];
66+
67+
changeTrackers.forEach(tracker => {
68+
tracker.sources.forEach((source) => {
69+
const changes = {};
70+
71+
// Find changes across all runs
72+
Object.values(data.runs).flatMap(runData =>
73+
findVersionChanges(
74+
tracker.pointsFilter ? tracker.pointsFilter(runData.data, source.url) : runData.data,
75+
tracker.versionKey
76+
)
77+
).forEach(change => {
78+
const changeKey = `${source.name}-${change.newVersion}`;
79+
if (!changes[changeKey] || change.date < changes[changeKey].date) {
80+
changes[changeKey] = change;
81+
}
82+
});
83+
84+
// Create annotation for each unique change
85+
Object.values(changes).forEach(change => {
86+
const annotationId = `${change.date}`;
87+
// If annotation at a given date already exists, update it
88+
if (options.plugins.annotation.annotations[annotationId]) {
89+
options.plugins.annotation.annotations[annotationId].label.content.push(
90+
tracker.formatLabel ?
91+
tracker.formatLabel(source.name, change.newVersion) :
92+
`${source.name}: ${change.newVersion}`
93+
);
94+
options.plugins.annotation.annotations[annotationId].borderColor = 'rgba(128, 128, 128, 0.8)';
95+
options.plugins.annotation.annotations[annotationId].borderWidth += 1;
96+
options.plugins.annotation.annotations[annotationId].label.backgroundColor = 'rgba(128, 128, 128, 0.9)';
97+
} else {
98+
options.plugins.annotation.annotations[annotationId] = {
99+
type: 'line',
100+
xMin: change.date,
101+
xMax: change.date,
102+
borderColor: source.color.border,
103+
borderWidth: 2,
104+
borderDash: [5, 5],
105+
label: {
106+
content: [
107+
tracker.formatLabel ?
108+
tracker.formatLabel(source.name, change.newVersion) :
109+
`${source.name}: ${change.newVersion}`
110+
],
111+
display: false,
112+
position: 'start',
113+
backgroundColor: source.color.background,
114+
z: 1,
115+
}
116+
}
117+
};
118+
});
119+
});
120+
});
121+
}
122+
123+
/**
124+
* Set up event listeners for annotation interactions
125+
* @param {Chart} chart - Chart.js instance
126+
* @param {CanvasRenderingContext2D} ctx - Canvas context
127+
* @param {Object} options - Chart.js options object
128+
*/
129+
function setupAnnotationListeners(chart, ctx, options) {
130+
// Add event listener for annotation clicks - display/hide label
131+
ctx.canvas.addEventListener('click', function(e) {
132+
const activeElements = chart.getElementsAtEventForMode(e, 'nearest', { intersect: true }, false);
133+
134+
// If no data point is clicked, check if an annotation was clicked
135+
if (activeElements.length === 0) {
136+
const rect = chart.canvas.getBoundingClientRect();
137+
const x = e.clientX - rect.left;
138+
139+
// Check if click is near any annotation line
140+
const annotations = options.plugins.annotation.annotations;
141+
Object.values(annotations).some(annotation => {
142+
// Get the position of the annotation line
143+
const xPos = chart.scales.x.getPixelForValue(annotation.xMin);
144+
145+
// Display label if click is near the annotation line (within 5 pixels)
146+
if (Math.abs(x - xPos) < 5) {
147+
annotation.label.display = !annotation.label.display;
148+
chart.update();
149+
return true; // equivalent to break in a for loop
150+
}
151+
return false;
152+
});
153+
}
154+
});
155+
156+
// Add mouse move handler to change cursor when hovering over annotations
157+
ctx.canvas.addEventListener('mousemove', function(e) {
158+
const rect = chart.canvas.getBoundingClientRect();
159+
const x = e.clientX - rect.left;
160+
161+
// Check if mouse is near any annotation line
162+
const annotations = options.plugins.annotation.annotations;
163+
const isNearAnnotation = Object.values(annotations).some(annotation => {
164+
const xPos = chart.scales.x.getPixelForValue(annotation.xMin);
165+
166+
if (Math.abs(x - xPos) < 5) {
167+
return true;
168+
}
169+
return false;
170+
});
171+
172+
// Change cursor style based on proximity to annotation
173+
ctx.canvas.style.cursor = isNearAnnotation ? 'pointer' : '';
174+
});
175+
176+
// Reset cursor when mouse leaves the chart area
177+
ctx.canvas.addEventListener('mouseleave', function() {
178+
ctx.canvas.style.cursor = '';
179+
});
180+
}
181+
182+
// Export functions to make them available to other modules
183+
window.ChartAnnotations = {
184+
findVersionChanges,
185+
addVersionChangeAnnotations,
186+
setupAnnotationListeners
187+
};

devops/scripts/benchmarks/html/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
<title>Benchmark Results</title>
1313
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
1414
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
15+
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation"></script>
1516
<script src="data.js"></script>
1617
<script src="config.js"></script>
18+
<script src="chart-annotations.js"></script>
1719
<script src="scripts.js"></script>
1820
<link rel="stylesheet" href="styles.css">
1921
</head>

devops/scripts/benchmarks/html/scripts.js

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ let layerComparisonsData;
1313
let latestRunsLookup = new Map();
1414
let pendingCharts = new Map(); // Store chart data for lazy loading
1515
let chartObserver; // Intersection observer for lazy loading charts
16+
let annotationsOptions = new Map(); // Global options map for annotations
1617

1718
// DOM Elements
1819
let runSelect, selectedRunsDiv, suiteFiltersContainer, tagFiltersContainer;
@@ -62,6 +63,16 @@ const colorPalette = [
6263
'rgb(210, 190, 0)',
6364
];
6465

66+
const annotationPalette = [
67+
'rgba(167, 109, 59, 0.8)',
68+
'rgba(185, 185, 60, 0.8)',
69+
'rgba(58, 172, 58, 0.8)',
70+
'rgba(158, 59, 158, 0.8)',
71+
'rgba(167, 93, 63, 0.8)',
72+
'rgba(163, 60, 81, 0.8)',
73+
'rgba(51, 148, 155, 0.8)',
74+
]
75+
6576
const nameColorMap = {};
6677
let colorIndex = 0;
6778

@@ -132,6 +143,8 @@ function createChart(data, containerId, type) {
132143
`Stddev: ${point.stddev.toFixed(2)} ${data.unit}`,
133144
`Git Hash: ${point.gitHash}`,
134145
`Compute Runtime: ${point.compute_runtime}`,
146+
`Bench hash: ${point.gitBenchHash?.substring(0, 7)}`,
147+
`Bench URL: ${point.gitBenchUrl}`,
135148
];
136149
} else {
137150
return [`${context.dataset.label}:`,
@@ -140,7 +153,10 @@ function createChart(data, containerId, type) {
140153
}
141154
}
142155
}
143-
}
156+
},
157+
annotation: type === 'time' ? {
158+
annotations: {}
159+
} : undefined
144160
},
145161
scales: {
146162
y: {
@@ -158,7 +174,7 @@ function createChart(data, containerId, type) {
158174
if (type === 'time') {
159175
options.interaction = {
160176
mode: 'nearest',
161-
intersect: false
177+
intersect: true // Require to hover directly over a point
162178
};
163179
options.onClick = (event, elements) => {
164180
if (elements.length > 0) {
@@ -180,6 +196,11 @@ function createChart(data, containerId, type) {
180196
maxTicksLimit: 10
181197
}
182198
};
199+
200+
// Add dependencies version change annotations
201+
if (Object.keys(data.runs).length > 0) {
202+
ChartAnnotations.addVersionChangeAnnotations(data, options);
203+
}
183204
}
184205

185206
const chartConfig = {
@@ -202,29 +223,15 @@ function createChart(data, containerId, type) {
202223

203224
const chart = new Chart(ctx, chartConfig);
204225
chartInstances.set(containerId, chart);
226+
227+
// Add annotation interaction handlers for time-series charts
228+
if (type === 'time') {
229+
ChartAnnotations.setupAnnotationListeners(chart, ctx, options);
230+
}
231+
205232
return chart;
206233
}
207234

208-
function createTimeseriesDatasets(data) {
209-
return Object.entries(data.runs).map(([name, runData], index) => ({
210-
label: runData.runName, // Use run name for legend
211-
data: runData.points.map(p => ({
212-
seriesName: runData.runName, // Use run name for tooltips
213-
x: p.date,
214-
y: p.value,
215-
gitHash: p.git_hash,
216-
gitRepo: p.github_repo,
217-
stddev: p.stddev
218-
})),
219-
borderColor: colorPalette[index % colorPalette.length],
220-
backgroundColor: colorPalette[index % colorPalette.length],
221-
borderWidth: 1,
222-
pointRadius: 3,
223-
pointStyle: 'circle',
224-
pointHoverRadius: 5
225-
}));
226-
}
227-
228235
function updateCharts() {
229236
const filterRunData = (chart) => ({
230237
...chart,
@@ -815,7 +822,9 @@ function addRunDataPoint(group, run, result, comparison, name = null) {
815822
stddev: result.stddev,
816823
gitHash: run.git_hash,
817824
gitRepo: run.github_repo,
818-
compute_runtime: run.compute_runtime
825+
compute_runtime: run.compute_runtime,
826+
gitBenchUrl: result.git_url,
827+
gitBenchHash: result.git_hash,
819828
});
820829

821830
return group;
@@ -997,6 +1006,11 @@ function initializeCharts() {
9971006
allRunNames = [...new Set(benchmarkRuns.map(run => run.name))];
9981007
latestRunsLookup = createLatestRunsLookup(benchmarkRuns);
9991008

1009+
// Create global options map for annotations
1010+
annotationsOptions = createAnnotationsOptions(benchmarkRuns);
1011+
// Make it available to the ChartAnnotations module
1012+
window.annotationsOptions = annotationsOptions;
1013+
10001014
// Set up active runs
10011015
const runsParam = getQueryParam('runs');
10021016
if (runsParam) {
@@ -1109,3 +1123,30 @@ function loadData() {
11091123
document.addEventListener('DOMContentLoaded', () => {
11101124
loadData();
11111125
});
1126+
1127+
// Process all benchmark runs to create a global options map for annotations
1128+
function createAnnotationsOptions(benchmarkRuns) {
1129+
const repoMap = new Map();
1130+
1131+
benchmarkRuns.forEach(run => {
1132+
run.results.forEach(result => {
1133+
if (result.git_url && !repoMap.has(result.git_url)) {
1134+
const suiteName = result.suite;
1135+
const colorIndex = repoMap.size % annotationPalette.length;
1136+
const backgroundColor = annotationPalette[colorIndex].replace('0.8', '0.9');
1137+
const color = {
1138+
border: annotationPalette[colorIndex],
1139+
background: backgroundColor
1140+
};
1141+
1142+
repoMap.set(result.git_url, {
1143+
name: suiteName,
1144+
url: result.git_url,
1145+
color: color,
1146+
});
1147+
}
1148+
});
1149+
});
1150+
1151+
return repoMap;
1152+
}

0 commit comments

Comments
 (0)