index.html (672 lines of code) (raw):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Performance Bugs</title>
<link href="https://fonts.googleapis.com/css2?family=Zilla+Slab:wght@400;700&display=swap" rel="stylesheet">
<link href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<link rel="stylesheet" href="assets/main-ui-style.css">
<style>
.row {
width: 90%;
margin-bottom: 30px;
}
.row-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 10px;
border-bottom: 3px double #000;
padding-bottom: 5px;
text-align: center;
}
.stats-row {
display: flex;
justify-content: space-between;
gap: 20px;
align: center;
width: 300px;
}
.charts-row {
display: flex;
justify-content: space-between;
gap: 20px;
}
.metrics-container {
width: 100%;
margin-bottom: 30px;
}
.metrics {
display: flex;
justify-content: space-around;
width: 100%;
text-align: center;
}
.metric-column {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #e0e0e0;
border-radius: 10px;
padding: 20px;
margin: 0 10px;
flex: 1;
cursor: pointer;
}
.metric-title {
font-size: 20px;
margin-bottom: 10px;
}
.metric-number {
font-size: 50px;
}
.canvas-column canvas {
height: 450px;
background-color: white;
border-radius: 10px;
}
.sub-row {
flex: 1;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
background-color: #ffffff;
border: 1px solid #ddd;
border-radius: 10px;
}
th, td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
font-size: 14px;
word-wrap: break-word;
}
th {
background-color: #f2f2f2;
}
tr:hover {
background-color: #f0f0f0;
cursor: pointer;
}
.bug-table-container {
table-layout: fixed;
margin-bottom: 30px;
max-height: 500px;
overflow-y: auto;
}
.search-container {
width: 100%;
display: flex;
justify-content: center;
margin-bottom: 20px;
}
#search-box {
width: 50%;
padding: 10px;
border-radius: 5px;
}
</style>
<script>
function displayMetrics() {
const regressionCount = window.regressions.length;
const untriagedCount = window.untriaged.length;
const highImpactCount = window.highImpact.length;
document.getElementById('regression-count').textContent = regressionCount;
document.getElementById('untriaged-count').textContent = untriagedCount;
document.getElementById('high-impact-count').textContent = highImpactCount;
}
function sortTable(columnIndex, tableId) {
const table = document.getElementById(tableId);
const tbody = table.getElementsByTagName('tbody')[0];
const rows = Array.from(tbody.getElementsByTagName('tr'));
const isAscending = table.getAttribute('data-sort-direction') === 'asc';
const direction = isAscending ? 1 : -1;
rows.sort((a, b) => {
const cellA = a.getElementsByTagName('td')[columnIndex].innerText.toLowerCase();
const cellB = b.getElementsByTagName('td')[columnIndex].innerText.toLowerCase();
if (!isNaN(cellA) && !isNaN(cellB)) {
return direction * (parseFloat(cellA) - parseFloat(cellB));
} else {
return direction * cellA.localeCompare(cellB);
}
});
while (tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
rows.forEach(row => tbody.appendChild(row));
table.setAttribute('data-sort-direction', isAscending ? 'desc' : 'asc');
}
function filterRegressionTable() {
// Split out any terms if a comma was used.
const searchInput = document.getElementById('search-box').value.toLowerCase();
const searchTerms = searchInput.split(',').map(term => term.trim());
const table = document.getElementById('regression-table');
const rows = table.getElementsByTagName('tbody')[0].getElementsByTagName('tr');
let count = 0;
Array.from(rows).forEach(row => {
const summaryCell = row.getElementsByTagName('td')[4];
const summaryText = summaryCell ? summaryCell.textContent.toLowerCase() : '';
// Check if any search term matches the summary text
const matches = searchTerms.some(term => summaryText.includes(term));
if (matches) {
row.style.display = '';
count++;
} else {
row.style.display = 'none';
}
});
const countElement = document.getElementById('bug-table-count');
countElement.innerText = `Results found: ${count}`;
}
function displayBugTables() {
const regressions = window.regressions.sort((a, b) => {
return (new Date(b.creation_time)) - (new Date(a.creation_time));
});
const regressionTableBody = document.querySelector('#regression-table tbody');
regressions.forEach(bug => {
const row = document.createElement('tr');
const creationDate = (new Date(bug.creation_time)).toISOString().split('T')[0];
const lastChangeTime = new Date(bug.last_change_time);
const currentTime = new Date();
const timeDifference = currentTime - lastChangeTime; // milliseconds
const lastUpdate = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); // days
row.innerHTML = `
<td>${bug.id}</td>
<td>${bug.severity}</td>
<td>${bug.priority}</td>
<td>${bug.component}</td>
<td>${bug.summary}</td>
<td>${creationDate}</td>
<td>${lastUpdate}</td>
`;
row.onclick = () => {
window.top.location.href = `https://bugzilla.mozilla.org/show_bug.cgi?id=${bug.id}`;
};
regressionTableBody.appendChild(row);
});
const countElement = document.getElementById('bug-table-count');
countElement.innerText = `Results found: ${regressions.length}`;
}
const backgroundColorPlugin = {
id: 'backgroundColor',
beforeDraw: (chart) => {
const {ctx, width, height} = chart;
ctx.fillStyle = '#ececec';
ctx.fillRect(0, 0, width, height);
}
};
function displayOSChart() {
const os_counts = window.allBugs.reduce((acc, bug) => {
acc[bug.op_sys] = (acc[bug.op_sys] || 0) + 1;
return acc;
}, {});
const labels = Object.keys(os_counts).map(key => `${key} (${os_counts[key]})`);
const data = Object.values(os_counts);
const navigateToBugzilla = (label) => {
const op_sys = label.split(' (')[0];
const url = `https://bugzilla.mozilla.org/buglist.cgi?f1=cf_performance_impact&o1=isnotempty&query_format=advanced&bug_status=__open__&op_sys=${encodeURIComponent(op_sys)}&f3=cf_performance_impact&o3=notequals&v3=none`;
window.top.location.href = url;
};
const ctx = document.getElementById('os-canvas').getContext('2d');
const osChart = new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data
}]
},
options: {
responsive: true,
onClick: (event, elements) => {
if (elements.length > 0) {
const chartElement = elements[0];
const label = osChart.data.labels[chartElement.index];
navigateToBugzilla(label);
}
},
plugins: {
legend: {
position: 'right',
onClick: (e, legendItem, legend) => {
const label = legendItem.text;
navigateToBugzilla(label);
},
labels: {
boxWidth: 20,
maxWidth: 300,
font: {
size: 14
}
}
}
}
},
plugins: [backgroundColorPlugin]
});
}
function displayComponentTable() {
const componentCounts = window.allBugs.reduce((acc, bug) => {
acc[bug.component] = (acc[bug.component] || 0) + 1;
return acc;
}, {});
const totalBugs = window.allBugs.length;
const components = Object.keys(componentCounts).map(component => ({
name: component,
count: componentCounts[component],
percentage: ((componentCounts[component] / totalBugs) * 100).toFixed(2)
}));
components.sort((a, b) => b.count - a.count);
const tableBody = document.querySelector('#component-table tbody');
components.forEach(component => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${component.name}</td>
<td>${component.count}</td>
<td>${component.percentage}%</td>
`;
row.onclick = () => {
const url = `https://bugzilla.mozilla.org/buglist.cgi?f1=cf_performance_impact&o1=isnotempty&query_format=advanced&bug_status=__open__&component=${encodeURIComponent(component.name)}&f3=cf_performance_impact&o3=notequals&v3=none`;
window.top.location.href = url;
};
tableBody.appendChild(row);
});
}
function displayHighImpactChart() {
const highImpactBugs = window.highImpact;
const componentCounts = highImpactBugs.reduce((acc, bug) => {
acc[bug.component] = (acc[bug.component] || 0) + 1;
return acc;
}, {});
const labels = Object.keys(componentCounts).map(key => `${key} (${componentCounts[key]})`);
const data = Object.values(componentCounts);
const navigateToBugzilla = (label) => {
const component = label.split(' (')[0];
const url = `https://bugzilla.mozilla.org/buglist.cgi?f1=cf_performance_impact&o1=equals&v1=high&query_format=advanced&bug_status=__open__&component=${encodeURIComponent(component)}`;
window.top.location.href = url;
};
const ctx = document.getElementById('high-impact-canvas').getContext('2d');
const highImpactComponentChart = new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data,
}]
},
options: {
responsive: true,
onClick: (event, elements) => {
if (elements.length > 0) {
const chartElement = elements[0];
const label = highImpactComponentChart.data.labels[chartElement.index];
navigateToBugzilla(label);
}
},
plugins: {
legend: {
position: 'right',
onClick: (e, legendItem, legend) => {
const label = legendItem.text;
navigateToBugzilla(label);
},
labels: {
boxWidth: 20,
maxWidth: 300,
font: {
size: 12
}
}
}
}
},
plugins: [backgroundColorPlugin]
});
}
function displayComponentChart() {
const componentCounts = window.allBugs.reduce((acc, bug) => {
acc[bug.component] = (acc[bug.component] || 0) + 1;
return acc;
}, {});
const labels = Object.keys(componentCounts).map(key => `${key} (${componentCounts[key]})`);
const data = Object.values(componentCounts);
const navigateToBugzilla = (label) => {
const component = label.split(' (')[0];
const url = `https://bugzilla.mozilla.org/buglist.cgi?f1=cf_performance_impact&o1=isnotempty&query_format=advanced&bug_status=__open__&component=${encodeURIComponent(component)}&f3=cf_performance_impact&o3=notequals&v3=none`;
window.top.location.href = url;
};
const ctx = document.getElementById('component-canvas').getContext('2d');
const componentChart = new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data,
}]
},
options: {
responsive: true,
onClick: (event, elements) => {
if (elements.length > 0) {
const chartElement = elements[0];
const label = componentChart.data.labels[chartElement.index];
navigateToBugzilla(label);
}
},
plugins: {
legend: {
position: 'bottom',
onClick: (e, legendItem, legend) => {
const label = legendItem.text;
navigateToBugzilla(label);
},
labels: {
boxWidth: 20,
maxWidth: 300,
font: {
size: 12
}
}
}
}
},
plugins: [backgroundColorPlugin]
});
}
function displayImpactChart() {
const highImpactLength = window.highImpact.length;
const medImpactLength = window.medImpact.length;
const lowImpactLength = window.lowImpact.length;
const untriagedLength = window.untriaged.length;
const needinfoLength = window.needinfo.length;
const labels = [
`High (${highImpactLength})`,
`Medium (${medImpactLength})`,
`Low (${lowImpactLength})`,
`Untriaged (${untriagedLength})`,
`Pending Needinfo (${needinfoLength})`,
];
const navigateToBugzilla = (label) => {
let impact = label.split(' ')[0];
// Perf-triage has a dedicated search query.
if (impact === 'Untriaged') {
const url = 'https://bugzilla.mozilla.org/buglist.cgi?query_based_on=Performance%20Triage&query_format=advanced&resolution=---&f1=OP&f2=cf_performance_impact&o2=equals&v2=%3F&f3=CP&f4=OP&f5=product&o5=equals&v5=Core&f6=component&o6=equals&v6=Performance&f7=keywords&o7=notsubstring&v7=meta&f8=cf_performance_impact&o8=isempty&f9=CP&f10=OP&f11=cf_performance_impact&o11=equals&v11=pending-needinfo&f12=flagtypes.name&o12=notsubstring&v12=needinfo&f13=CP&j_top=OR&order=Bug%20Number&include_fields=id&include_fields=summary&include_fields=status'
window.top.location.href = url;
} else {
if (impact === 'Pending') {
impact = 'pending-needinfo';
}
const url = `https://bugzilla.mozilla.org/buglist.cgi?f1=cf_performance_impact&v1=${impact}&o1=equals&query_format=advanced&bug_status=__open__`
window.top.location.href = url;
}
};
const ctx = document.getElementById('impact-canvas').getContext('2d');
const impactChart = new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: [highImpactLength, medImpactLength, lowImpactLength, untriagedLength, needinfoLength],
backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#4D5360'],
hoverBackgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#4D5360']
}]
},
options: {
responsive: true,
onClick: (event, elements) => {
if (elements.length > 0) {
const chartElement = elements[0];
const label = impactChart.data.labels[chartElement.index];
navigateToBugzilla(label);
}
},
plugins: {
legend: {
position: 'right',
onClick: (e, legendItem, legend) => {
const label = legendItem.text;
navigateToBugzilla(label);
},
labels: {
boxWidth: 20,
maxWidth: 300,
font: {
size: 14
}
},
}
}
},
plugins: [backgroundColorPlugin]
});
}
function displayPerfKeywordsChart() {
const perfCounts = window.allBugs.reduce((acc, bug) => {
let hasPerfKeyword = false;
bug.keywords.forEach(keyword => {
if (keyword.startsWith('perf:')) {
hasPerfKeyword = true;
acc[keyword] = (acc[keyword] || 0) + 1;
}
});
if (!hasPerfKeyword) {
acc['unspecified'] = (acc['unspecified'] || 0) + 1;
}
return acc;
}, {});
const labels = Object.keys(perfCounts).map(key => `${key.replace('perf:', '')} (${perfCounts[key]})`);
const data = Object.values(perfCounts);
const navigateToBugzilla = (label) => {
const value = label.split(' (')[0];
if (value === 'unspecified') {
const url = 'https://bugzilla.mozilla.org/buglist.cgi?f2=cf_performance_impact&bug_status=__open__&f1=keywords&query_format=advanced&o1=notsubstring&v1=perf%3A&o2=isnotempty&f3=cf_performance_impact&o3=notequals&v3=none';
window.top.location.href = url;
} else {
const keyword = encodeURIComponent(`perf:${value}`);
const url = `https://bugzilla.mozilla.org/buglist.cgi?o1=substring&query_format=advanced&bug_status=__open__&f2=cf_performance_impact&o2=isnotempty&v1=${keyword}&f1=keywords&f3=cf_performance_impact&o3=notequals&v3=none`
window.top.location.href = url;
}
};
const ctx = document.getElementById('keyword-canvas').getContext('2d');
const perfKeywordChart = new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data,
}]
},
options: {
responsive: true,
onClick: (event, elements) => {
if (elements.length > 0) {
const chartElement = elements[0];
const label = perfKeywordChart.data.labels[chartElement.index];
navigateToBugzilla(label);
}
},
plugins: {
legend: {
position: 'right',
onClick: (e, legendItem, legend) => {
const label = legendItem.text;
navigateToBugzilla(label);
},
labels: {
boxWidth: 20,
maxWidth: 300,
font: {
size: 14
}
}
}
}
},
plugins: [backgroundColorPlugin]
});
}
function loadBugzillaData() {
const bugzillaUrl = 'https://raw.githubusercontent.com/mozilla/performance-data/refs/heads/main/bugzilla-data-all.json';
fetch(bugzillaUrl)
.then(response => response.json())
.then(data => {
// bugzilla sorts from oldest->newest, so reverse the arrays.
window.highImpact = data.high.bugs.reverse();
window.medImpact = data.medium.bugs.reverse();
window.lowImpact = data.low.bugs.reverse();
window.untriaged = data.untriaged.bugs.reverse();
window.needinfo = data.needinfo.bugs.reverse();
window.regressions = data.regressions.bugs.reverse();
window.allBugs = [...window.highImpact, ...window.medImpact, ...window.lowImpact, ...window.needinfo, ...window.regressions];
window.allBugs.sort((a, b) => (new Date(b.last_change_time)) - (new Date(a.last_change_time)));
// Remove any duplicate bugs.
(bugs => {
const seen = new Set();
return bugs.filter(bug => {
if (seen.has(bug.id)) {
return false;
} else {
seen.add(bug.id);
return true;
}
});
})(window.allBugs);
displayMetrics();
displayPerfKeywordsChart();
displayImpactChart();
displayHighImpactChart();
displayComponentTable();
displayOSChart();
displayBugTables();
})
.catch(error => console.error('Error fetching Bugzilla data:', error));
}
document.addEventListener("DOMContentLoaded", () => {
loadBugzillaData();
document.getElementById('search-box').addEventListener('keyup', filterRegressionTable);
});
</script>
</head>
<body>
<div class="top">
<div class="top-title-container">
<div class="top-title">
<b>moz://a</b> performance portal
</div>
</div>
<a href="https://firefox-source-docs.mozilla.org/performance/reporting_a_performance_problem.html" class="btn" title="Report a Performance Bug">
<i class="fa-solid fa-bug"></i> Report Performance Issue
</a>
</div>
<div class="main-content">
<aside class="main-sidebar" id="main-sidebar">
</aside>
<div class="content">
<div class="row">
<div class="row-title">Current Bug Status</div>
<div class="metrics-container">
<div class="metrics">
<div class="metric-column" onclick="window.top.location.href='https://bugzilla.mozilla.org/buglist.cgi?query_based_on=Performance%20Triage&query_format=advanced&resolution=---&f1=OP&f2=cf_performance_impact&o2=equals&v2=%3F&f3=CP&f4=OP&f5=product&o5=equals&v5=Core&f6=component&o6=equals&v6=Performance&f7=keywords&o7=notsubstring&v7=meta&f8=cf_performance_impact&o8=isempty&f9=CP&f10=OP&f11=cf_performance_impact&o11=equals&v11=pending-needinfo&f12=flagtypes.name&o12=notsubstring&v12=needinfo&f13=CP&j_top=OR&order=Bug%20Number&include_fields=id&include_fields=summary&include_fields=status'">
<div class="metric-title">Untriaged</div>
<div class="metric-number" id="untriaged-count">0</div>
</div>
<div class="metric-column" onclick="window.top.location.href='https://bugzilla.mozilla.org/buglist.cgi?query_format=advanced&o1=substring&f1=keywords&classification=Client%20Software&classification=Developer%20Infrastructure&classification=Components&classification=Server%20Software&classification=Other&o2=anywordssubstr&f2=keywords&v2=perf-alert&v1=regression&resolution=---'">
<div class="metric-title">Regressions</div>
<div class="metric-number" id="regression-count">0</div>
</a>
</div>
<div class="metric-column" onclick="window.top.location.href=`https://bugzilla.mozilla.org/buglist.cgi?f1=cf_performance_impact&o1=equals&v1=high&query_format=advanced&bug_status=__open__`">
<div class="metric-title">High Impact</div>
<div class="metric-number" id="high-impact-count">0</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="row-title" id="regression-title">Open Perf-Alert Regressions</div>
<div class="search-container">
<input type="text" id="search-box" placeholder="Search by summary">
</div>
<div class="bug-table-container">
<div id="bug-table-count">Results found:</div>
<table id="regression-table">
<thead>
<tr>
<th onclick="sortTable(0, 'regression-table')">ID</th>
<th onclick="sortTable(1, 'regression-table')">Severity</th>
<th onclick="sortTable(2, 'regression-table')">Priority</th>
<th onclick="sortTable(3, 'regression-table')">Component</th>
<th onclick="sortTable(4, 'regression-table')">Summary</th>
<th onclick="sortTable(5, 'regression-table')">Creation Date</th>
<th onclick="sortTable(6, 'regression-table')">Last Update (days)</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="row">
<div class="charts-row">
<div class="sub-row">
<div class="row-title">Performance Impact Distribution</div>
<div class="charts-row">
<div class="canvas-column">
<canvas id="impact-canvas"></canvas>
</div>
</div>
</div>
<div class="sub-row">
<div class="row-title">High Impact Distribution</div>
<div class="charts-row">
<div class="canvas-column">
<canvas id="high-impact-canvas"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="charts-row">
<div class="sub-row">
<div class="row-title">OS Distribution</div>
<div class="charts-row">
<div class="canvas-column">
<canvas id="os-canvas"></canvas>
</div>
</div>
</div>
<div class="sub-row">
<div class="row-title">Performance Type Distribution</div>
<div class="charts-row">
<div class="canvas-column">
<canvas id="keyword-canvas"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="row-title">Component Distribution</div>
<div class="bug-table-container" style="max-height: 500px">
<table id="component-table">
<thead>
<tr>
<th>Component</th>
<th>Number of Bugs</th>
<th>Percentage of Bugs</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div> <!-- content -->
</div> <!-- main-content -->
<script src="assets/main-ui.js"></script>
</body>
</html>