assets/android-metrics.js (434 lines of code) (raw):
window.telemetryData = [];
// Set some defaults.
window.platformStatus = 'Android';
window.historyStatus = 30;
window.allCharts = [];
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 setHistory(history) {
// Find max number of days.
const latestDate = new Date(window.data.at(-1).date);
const furthestDate = new Date(window.data.at(0).date);
const maxDays = (latestDate - furthestDate) / (1000 * 60 * 60 * 24);
if (history > maxDays) {
history = Math.round(maxDays);
}
window.historyStatus = history;
const input = document.getElementById('history-select');
input.value = history;
displayCharts();
}
function setPlatform(platform) {
window.platformStatus = platform;
displayCharts();
}
function writeContentTitle() {
const title = document.getElementById('content-title');
title.innerText = `${window.platformStatus} / ${window.historyStatus} days`;
}
function calculateDelta(values) {
const n = values.length;
// Create x values (0 to n-1) representing the time sequence of data points
const xValues = Array.from({ length: n }, (_, i) => i);
// Calculate the averages of x and y (the subset values)
const xAvg = xValues.reduce((sum, x) => sum + x, 0) / n;
const yAvg = values.reduce((sum, y) => sum + y, 0) / n;
// Calculate the slope (m) of the line of best fit using the least squares method
const numerator = xValues.reduce((sum, x, i) => sum + (x - xAvg) * (values[i] - yAvg), 0);
const denominator = xValues.reduce((sum, x) => sum + Math.pow(x - xAvg, 2), 0);
const slope = numerator / denominator;
// Calculate the y-intercept (b) of the line of best fit
const intercept = yAvg - slope * xAvg;
// Calculate the predicted y-values (trend line) for the first and last x-values
const firstY = intercept; // y-value at x = 0 (first data point)
const lastY = slope * (n - 1) + intercept; // y-value at x = n - 1 (last data point)
// Calculate the percentage improvement between the first and last points on the trend line
const improvement = ((lastY - firstY) / firstY) * 100;
return improvement.toFixed(2);
}
function displayMetrics(id, title1, values1, title2, values2) {
function getDeltaColor(improvement) {
if (improvement > 5)
return "red";
else if (improvement < -5)
return "green";
else
return "black";
}
const improvement1 = calculateDelta(values1);
const improvement2 = calculateDelta(values2);
document.getElementById(id).innerHTML = `
<div class="metric-item">${title1} Δ: <font style="color: ${getDeltaColor(improvement1)};">${improvement1 > 0 ? "+" + improvement1 : improvement1}%</font></div>
<div class="metric-item">${title2} Δ: <font style="color: ${getDeltaColor(improvement2)};">${improvement2 > 0 ? "+" + improvement2 : improvement2}%</font></div>
`;
}
function plotChart(id, dataset, unit) {
const ctx = document.getElementById(id+"-canvas").getContext('2d');
let chart = new Chart(ctx, {
type: 'scatter',
data: {
datasets: dataset
},
options: {
scales: {
x: {
type: 'time',
time: { unit: 'day', tooltipFormat: 'll' },
title: { display: true, text: 'Timestamp' }
},
y: {
beginAtZero: false,
title: { display: true, text: `${unit}` }
}
},
},
plugins: [backgroundColorPlugin]
});
//displayMetrics(id+"-metrics", title1, values1, title2, values2);
window.allCharts.push(chart);
}
function updateChart() {
Chart.helpers.each(Chart.instances, function (instance) {
instance.options.scales.xAxes[0].time.min = leftEnd;
instance.options.scales.xAxes[0].time.max = rightEnd;
instance.update();
});
}
function getAppColor(app) {
switch(app) {
case "fenix":
case "firefox":
return 'rgba(54, 162, 235, 1)';
case "chrome-m":
case "chrome":
return 'rgba(255, 99, 132, 1)'
case "cstm-car-m":
return 'purple';
}
return "green";
}
function displayChartForMetric(data, metric, name, unit) {
const filteredData = data.filter(row => row.test === metric);
const groupedData = {};
let url="https://treeherder.mozilla.org/perfherder/graphs?highlightAlerts=1&highlightChangelogData=1&highlightCommonAlerts=0&timerange=2592000"
const signatureIds = new Set()
filteredData.forEach(row => {
if (url !== undefined && !signatureIds.has(row.signature_id)) {
let signature_id = row.signature_id;
let framework_id = row.framework_id;
let repository_id = row.repository_id;
if (signature_id && framework_id && repository_id) {
url += `&series=mozilla-central,${signature_id},${repository_id},${framework_id}`
} else {
url = undefined;
}
signatureIds.add(signature_id);
}
});
// Update the title to a link to the perfherder page.
if (url) {
const title = document.getElementById(name+"-section");
if (title) {
let link = document.createElement("a");
link.href = url;
link.style.textDecoration = "none";
title.parentNode.insertBefore(link, title);
link.appendChild(title);
}
}
// Group data by application
filteredData.forEach(item => {
if (!groupedData[item.application]) {
groupedData[item.application] = [];
}
groupedData[item.application].push({
x: new Date(item.date), // Convert date to Date object for x-axis
y: item.value // Use value for y-axis
});
});
dataset = Object.keys(groupedData).map((app, index) => ({
label: app,
data: groupedData[app],
fill: false,
backgroundColor: getAppColor(app),
borderWidth: 1,
tension: 0.1,
}));
// Only add trendline's when there are enough data points.
dataset.forEach(ds => {
if (ds.data.length > 5) {
ds["trendlineLinear"] = {
colorMin: getAppColor(ds.label),
colorMax: getAppColor(ds.label),
style: "rgba(255,105,180, .8)",
lineStyle: "dotted",
width: 2
}
}
});
plotChart(metric, dataset, unit);
}
function displayCharts() {
while (window.allCharts.length > 0) {
window.allCharts.pop().destroy();
}
const history = window.historyStatus;
const latestDate = new Date(window.data.at(-1).date);
let cutoffDate = new Date(latestDate);
cutoffDate.setDate(latestDate.getDate() - history);
cutoffDate = cutoffDate.toISOString().split('T')[0];
const filteredData = window.data.filter(row => row.date >= cutoffDate);
window.tests.forEach(test => {
test.metric.forEach(metric => {
displayChartForMetric(filteredData, metric, test.name, test.unit);
});
});
}
const backgroundColorPlugin = {
id: 'backgroundColor',
beforeDraw: (chart) => {
const {ctx, width, height} = chart;
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, width, height);
}
};
function fixupStartupTests(data) {
data.forEach(row => {
if (row.suite === 'AndroidStartup:fenix') {
row.suite = 'AndroidStartup';
row.application = 'fenix';
} else if (row.suite === 'AndroidStartup:chrome-m') {
row.suite = 'AndroidStartup';
row.application = 'chrome-m';
} else if (row.test === 'applink_startup' && row.suite === 'newssite-applink-startup' && row.platform === 'android-hw-a55-14-0-aarch64-shippable') {
row.test = 'newssite-applink-startup-a55';
} else if (row.test === 'applink_startup' && row.suite === 'newssite-applink-startup' && row.platform === 'android-hw-p6-13-0-aarch64-shippable') {
row.test = 'newssite-applink-startup-p6';
} else if (row.test === 'applink_startup' && row.suite === 'newssite-applink-startup' && row.platform === 'android-hw-s24-14-0-aarch64-shippable') {
row.test = 'newssite-applink-startup-s24';
} else if (row.test === 'applink_startup' && row.suite === 'shopify-applink-startup') {
row.test = 'shopify-applink-startup';
}
});
}
function fixupPageloadTests(data) {
data.forEach(row => {
if (row.category === 'pageload') {
row.test = `${row.suite}-${row.test}`
}
});
}
function geometricMean(numbers) {
const positiveNumbers = numbers.filter(num => num > 0);
if (positiveNumbers.length === 0) return 0;
const product = positiveNumbers.reduce((acc, num) => acc * num, 1);
return Math.pow(product, 1 / positiveNumbers.length);
}
function fixupResourceTests(data) {
const resourceData = data.filter(row => row.category === 'resource');
let groupedObjects = {};
resourceData.forEach(row => {
const fields = row.test.split('-');
const resource = fields[0]; // "rss", "pss", "cpuTime"
const process = fields[1]; // "gpu", "main", "tab", "total"
const key = `${resource}-${process}-${row.application}-${row.date}`;
if (!groupedObjects[key]) {
groupedObjects[key] = {
application: row.application,
date: row.date,
category: row.category,
suite: row.suite,
metrics: []
}
}
groupedObjects[key].metrics.push(row.value);
});
Object.keys(groupedObjects).forEach(key => {
const entry = groupedObjects[key];
const avg = entry.metrics.reduce((acc, value) => acc + value, 1) / (entry.metrics.length);
const fields = key.split('-');
const resource = fields[0];
const process = fields[1];
data.push({
application: entry.application,
category: entry.category,
date: entry.date,
suite: entry.suite,
test: `${resource}-${process}-geomean`, // e.g., "rss-geomean", "pss-geomean"
value: avg
});
});
}
async function loadData(dataUrl) {
try {
// Fetch the gzipped file as a binary array buffer
const response = await fetch(dataUrl);
const compressedData = await response.arrayBuffer();
// Decompress the data using pako
const decompressedData = pako.inflate(new Uint8Array(compressedData), { to: 'string' });
// Parse the decompressed string as JSON
let data = JSON.parse(decompressedData).query_result.data.rows;
fixupStartupTests(data);
fixupPageloadTests(data);
fixupResourceTests(data);
window.data = data;
window.data.sort((a, b) => new Date(a.date) - new Date(b.date));
displayCharts();
displayTable();
} catch (error) {
console.error('Error loading the JSON file:', error);
}
}
function generateContent(dataUrl) {
const historyInput = document.getElementById('history-select');
historyInput.value = window.historyStatus;
loadData(dataUrl);
}
// Calculate the differences between firefox & chrome for each test and
// display them in a table. We ignore any results that don't have a
// chrome average.
function displayTable() {
const results = [];
window.tests.forEach(test => {
metric = test.metric[0];
const firefoxData = window.data.filter(
item => item.test === metric && (item.application === 'firefox' || item.application === 'fenix')
);
const chromeData = window.data.filter(
item => item.test === metric && item.application === 'chrome-m'
);
const carData = window.data.filter(
item => item.test === metric && item.application === 'cstm-car-m'
);
const firefoxAvg = calculateRecentAverage(firefoxData);
const chromeAvg = calculateRecentAverage(carData.length > 0 ? carData : chromeData);
const difference = ((chromeAvg - firefoxAvg) / firefoxAvg) * 100;
const monthAgoFirefoxAvg = calculateMonthAgoAverage(firefoxData);
const monthAgoChromeAvg = calculateMonthAgoAverage(carData.length > 0 ? carData : chromeData);
const monthAgoDifference = ((monthAgoChromeAvg - monthAgoFirefoxAvg) / monthAgoFirefoxAvg) * 100;
if (chromeAvg !== 0) {
results.push({
name: test.name,
unit: test.unit,
test: metric,
firefoxAvg: firefoxAvg,
chromeAvg: chromeAvg,
difference: difference.toFixed(1),
monthAgoFirefoxAvg: monthAgoFirefoxAvg,
monthAgoChromeAvg: monthAgoChromeAvg,
monthAgoDifference: monthAgoDifference.toFixed(1)
});
}
});
displayResultsInTable(results);
}
// Take the most recent 7 entries and average.
function calculateRecentAverage(data) {
if (data.length === 0) return 0;
data.sort((a, b) => new Date(b.date) - new Date(a.date));
const currentDate = new Date(data[0].date);
const cutoffDate = new Date(currentDate);
cutoffDate.setDate(cutoffDate.getDate() - 6);
const last7DaysData = data.filter(item => {
const itemDate = new Date(item.date);
return itemDate >= cutoffDate && itemDate <= currentDate;
});
const total = last7DaysData.reduce((sum, item) => sum + item.value, 0);
return last7DaysData.length > 0 ? total / last7DaysData.length : 0;
}
function calculateMonthAgoAverage(data) {
if (data.length === 0) return 0;
data.sort((a, b) => new Date(b.date) - new Date(a.date));
const mostRecentDate = new Date(data[0].date);
const currentDate = new Date(mostRecentDate);
currentDate.setDate(currentDate.getDate() - 28);
const cutoffDate = new Date(currentDate);
cutoffDate.setDate(cutoffDate.getDate() - 6);
const last7DaysData = data.filter(item => {
const itemDate = new Date(item.date);
return itemDate >= cutoffDate && itemDate <= currentDate;
});
const total = last7DaysData.reduce((sum, item) => sum + item.value, 0);
return last7DaysData.length > 0 ? total / last7DaysData.length : 0;
}
// Display results in the summary table.
function displayResultsInTable(results) {
const table = document.querySelector('#summary-table tbody');
results.forEach(result => {
const row = document.createElement('tr');
if (result.unit.includes('score')) {
result.difference = -result.difference;
result.monthAgoDifference = -result.monthAgoDifference;
}
let recentClass = "";
if (result.difference < -5) {
recentClass='negative-difference';
} else if (result.difference > 5) {
recentClass='negative-positive';
}
let monthAgoClass= "";
if (result.monthAgoDifference < -5) {
monthAgoClass='negative-difference';
} else if (result.difference > 5) {
monthAgoClass='negative-positive';
}
row.onclick = () => {
window.location.hash = `${result.name}-section`;
};
row.innerHTML = `
<td class="${recentClass}" >${result.name} (${result.unit})</td>
<td class="${recentClass}" style="border-left: 1px solid #ddd;">${result.firefoxAvg.toFixed(2)}</td>
<td class="${recentClass}" >${result.chromeAvg.toFixed(2)}</td>
<td class="${recentClass}" >${result.difference}</td>
<td class="${monthAgoClass}" style="border-left: 1px solid #ddd;">${result.monthAgoFirefoxAvg.toFixed(2)}</td>
<td class="${monthAgoClass}" >${result.monthAgoChromeAvg.toFixed(2)}</td>
<td class="${monthAgoClass}" >${result.monthAgoDifference}</td>
`;
table.appendChild(row);
});
}
function createSummaryTable(tests) {
calculate7DayAverage();
}
function createSidebarSections(tests) {
var sectionsHTML = '';
sectionsHTML += `<a href="#summary-section">Summary</a>\n`;
tests.forEach(function(test) {
sectionsHTML += `<a href="#${test.name}-section">${test.name}</a>\n`;
});
document.querySelector('.sections').innerHTML = sectionsHTML;
}
function createChartsContent(tests) {
var chartsHTML = '';
tests.forEach(function(test) {
chartsHTML += `
<div class="row-title" id="${test.name}-section">${test.name}</div>
<div class="content-row">
`;
test.metric.forEach(metric => {
chartsHTML += `
<div class="canvas-column">
<div id="${metric}-metrics" class="metric-container"></div>
<canvas id="${metric}-canvas"></canvas>
</div>
`;
});
chartsHTML += `
</div>
`;
});
document.querySelector('.charts-content').innerHTML = chartsHTML;
}