function formatTime(ns) {
if (ns < 1000) {
return `${ns.toFixed(2)} ns`;
} else if (ns < 1000000) {
return `${(ns / 1000).toFixed(2)} μs`;
} else if (ns < 1000000000) {
return `${(ns / 1000000).toFixed(2)} ms`;
} else {
return `${(ns / 1000000000).toFixed(2)} s`;
}
}
function parseBenchmarkName(name) {
const match = name.match(/^(small|medium|large)_(.+)$/);
if (!match) {
return {
size: 'unknown',
filename: name,
baseName: name,
extension: '',
displayName: name
};
}
const size = match[1];
const filename = match[2];
const extMatch = filename.match(/\.([a-z]+)$/i);
const extension = extMatch ? extMatch[1].toLowerCase() : '';
const baseName = extension ? filename.slice(0, -(extension.length + 1)) : filename;
return {
size: size,
filename: filename,
baseName: baseName,
extension: extension,
displayName: filename
};
}
function getFileTypeCategory(extension) {
const categories = {
'vbp': 'Project Files',
'cls': 'Class Files',
'bas': 'Module Files',
'frm': 'Form Files',
'frx': 'Form Resources'
};
return categories[extension] || 'Other';
}
function formatBenchmarkName(name) {
const parsed = parseBenchmarkName(name);
return parsed.displayName;
}
function getBenchmarkId(name) {
return 'benchmark-' + name.replace(/[^a-z0-9]/gi, '-').toLowerCase();
}
function getTrendInfo(benchmarkName, history) {
if (!history || !history.benchmarks_summary) {
return null;
}
const summary = history.benchmarks_summary[benchmarkName];
if (!summary || !summary.trend) {
return null;
}
return summary.trend;
}
function formatTrendBadge(trend) {
if (!trend) {
return '';
}
const icons = {
'improving': '📈 ↓',
'degrading': '📉 ↑',
'stable': '→'
};
const icon = icons[trend.direction] || '→';
const changeText = Math.abs(trend.change_percent).toFixed(2);
return `
<span class="trend-badge ${trend.direction}">
${icon} ${changeText}%
</span>
`;
}
async function loadBenchmarks() {
try {
const response = await fetch('assets/data/benchmarks.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
let history = null;
try {
const historyResponse = await fetch('assets/data/benchmarks-history.json');
if (historyResponse.ok) {
history = await historyResponse.json();
}
} catch (e) {
console.log('No historical data available yet');
}
displayBenchmarks(data, history);
if (history && history.snapshots && history.snapshots.length >= 2) {
renderTrendsChart(history, 30); setupTimeRangeSelector(history);
}
} catch (error) {
showError(error.message);
}
}
function displayBenchmarks(data, history) {
const benchmarks = data.benchmarks || [];
if (benchmarks.length === 0) {
showError('No benchmark data available');
return;
}
if (history && history.snapshots) {
const snapshotCount = history.snapshots.length;
const lastUpdated = history.last_updated;
const summaryContainer = document.getElementById('overall-summary');
if (snapshotCount > 1) {
const historyCard = document.createElement('div');
historyCard.className = 'summary-card history-card';
historyCard.innerHTML = `
<h3>📊 Historical Data</h3>
<div class="summary-value">${snapshotCount}</div>
<div class="summary-label">snapshots</div>
<div class="summary-sublabel" style="font-size: 0.8em; opacity: 0.7; margin-top: 4px;">
Last: ${new Date(lastUpdated).toLocaleDateString()}
</div>
`;
summaryContainer.insertBefore(historyCard, summaryContainer.firstChild);
}
}
const grouped = {};
benchmarks.forEach(benchmark => {
const parsed = parseBenchmarkName(benchmark.name);
const fileType = getFileTypeCategory(parsed.extension);
const size = parsed.size.charAt(0).toUpperCase() + parsed.size.slice(1); const groupKey = `${fileType} - ${size}`;
if (!grouped[groupKey]) {
grouped[groupKey] = {};
}
if (!grouped[groupKey][parsed.baseName]) {
grouped[groupKey][parsed.baseName] = [];
}
grouped[groupKey][parsed.baseName].push({
...benchmark,
parsed: parsed
});
});
Object.keys(grouped).forEach(groupKey => {
Object.keys(grouped[groupKey]).forEach(baseName => {
grouped[groupKey][baseName].sort((a, b) => a.mean - b.mean);
});
});
const times = benchmarks.map(b => b.mean);
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const fastest = benchmarks.reduce((min, b) => b.mean < min.mean ? b : min);
const slowest = benchmarks.reduce((max, b) => b.mean > max.mean ? b : max);
const fileTypeStats = {};
Object.keys(grouped).forEach(groupKey => {
const group = grouped[groupKey];
const groupBenchmarks = [];
Object.keys(group).forEach(baseName => {
groupBenchmarks.push(...group[baseName]);
});
const [fileType] = groupKey.split(' - ');
if (!fileTypeStats[fileType]) {
fileTypeStats[fileType] = {
count: 0,
benchmarks: []
};
}
fileTypeStats[fileType].count += groupBenchmarks.length;
fileTypeStats[fileType].benchmarks.push(...groupBenchmarks);
});
const summaryContainer = document.getElementById('overall-summary');
summaryContainer.innerHTML = `
<div class="summary-card">
<h3>Total Benchmarks</h3>
<div class="summary-value">${benchmarks.length}</div>
</div>
${Object.keys(fileTypeStats).sort((a, b) => {
const order = {
'Project Files': 1,
'Class Files': 2,
'Module Files': 3,
'Form Files': 4,
'Form Resources': 5,
'Other': 6
};
return (order[a] || 999) - (order[b] || 999);
}).map(fileType => {
const stats = fileTypeStats[fileType];
const icon = {
'Project Files': '📦',
'Class Files': '📋',
'Module Files': '📄',
'Form Files': '🖼️',
'Form Resources': '🗂️',
'Other': '📁'
}[fileType] || '📁';
return `
<div class="summary-card type-card">
<h3>${icon} ${fileType}</h3>
<div class="summary-value">${stats.count}</div>
<div class="summary-label">benchmarks</div>
</div>
`;
}).join('')}
`;
const container = document.getElementById('benchmark-cards');
const sortedGroupKeys = Object.keys(grouped).sort((a, b) => {
const [typeA, sizeA] = a.split(' - ');
const [typeB, sizeB] = b.split(' - ');
const typeOrder = {
'Project Files': 1,
'Class Files': 2,
'Module Files': 3,
'Form Files': 4,
'Form Resources': 5,
'Other': 6
};
const sizeOrder = { 'Small': 1, 'Medium': 2, 'Large': 3, 'Unknown': 4 };
const typeCompare = (typeOrder[typeA] || 999) - (typeOrder[typeB] || 999);
if (typeCompare !== 0) return typeCompare;
return (sizeOrder[sizeA] || 999) - (sizeOrder[sizeB] || 999);
});
container.innerHTML = sortedGroupKeys.map(groupKey => {
const group = grouped[groupKey];
const baseNames = Object.keys(group).sort();
const sectionBenchmarks = [];
baseNames.forEach(baseName => {
sectionBenchmarks.push(...group[baseName]);
});
const sectionTimes = sectionBenchmarks.map(b => b.mean);
const sectionAvg = sectionTimes.reduce((a, b) => a + b, 0) / sectionTimes.length;
const sectionFastest = sectionBenchmarks.reduce((min, b) => b.mean < min.mean ? b : min);
const sectionSlowest = sectionBenchmarks.reduce((max, b) => b.mean > max.mean ? b : max);
const sectionSummary = `
<div class="section-summary">
<div class="section-stat">
<span class="stat-label">Count</span>
<span class="stat-value">${sectionBenchmarks.length}</span>
</div>
<div class="section-stat">
<span class="stat-label">Average</span>
<span class="stat-value">${formatTime(sectionAvg)}</span>
</div>
<a href="#${getBenchmarkId(sectionFastest.name)}" class="section-stat section-stat-link">
<span class="stat-label">Fastest</span>
<span class="stat-value">${formatTime(sectionFastest.mean)}</span>
<span class="stat-sublabel">${sectionFastest.parsed.displayName}</span>
</a>
<a href="#${getBenchmarkId(sectionSlowest.name)}" class="section-stat section-stat-link">
<span class="stat-label">Slowest</span>
<span class="stat-value">${formatTime(sectionSlowest.mean)}</span>
<span class="stat-sublabel">${sectionSlowest.parsed.displayName}</span>
</a>
</div>
`;
const cardsHtml = baseNames.map(baseName => {
const items = group[baseName];
const hasMultiple = items.length > 1;
if (hasMultiple) {
const itemsHtml = items.map((benchmark, idx) => {
const stdDevPercent = (benchmark.std_dev / benchmark.mean * 100).toFixed(2);
return `
<div class="benchmark-card" id="${getBenchmarkId(benchmark.name)}">
<div class="benchmark-header">
<h4 class="benchmark-name">Run #${idx + 1}</h4>
${formatTrendBadge(getTrendInfo(benchmark.name, history))}
</div>
<div class="benchmark-metrics">
<div class="metric">
<span class="metric-label">Mean</span>
<span class="metric-value">${formatTime(benchmark.mean)}</span>
</div>
<div class="metric">
<span class="metric-label">Median</span>
<span class="metric-value">${formatTime(benchmark.median)}</span>
</div>
<div class="metric">
<span class="metric-label">Std Dev</span>
<span class="metric-value">${formatTime(benchmark.std_dev)} <span class="metric-percent">(±${stdDevPercent}%)</span></span>
</div>
</div>
<div class="benchmark-bar">
<div class="benchmark-bar-fill" style="width: ${(benchmark.mean / slowest.mean * 100).toFixed(2)}%"></div>
</div>
</div>
`;
}).join('');
return `
<div class="benchmark-subsection">
<h3 class="subsection-header">${items[0].parsed.displayName}</h3>
<div class="benchmark-subsection-cards">
${itemsHtml}
</div>
</div>
`;
} else {
const benchmark = items[0];
const stdDevPercent = (benchmark.std_dev / benchmark.mean * 100).toFixed(2);
return `
<div class="benchmark-card" id="${getBenchmarkId(benchmark.name)}">
<div class="benchmark-header">
<h3 class="benchmark-name">${benchmark.parsed.displayName}</h3>
${formatTrendBadge(getTrendInfo(benchmark.name, history))}
</div>
<div class="benchmark-metrics">
<div class="metric">
<span class="metric-label">Mean</span>
<span class="metric-value">${formatTime(benchmark.mean)}</span>
</div>
<div class="metric">
<span class="metric-label">Median</span>
<span class="metric-value">${formatTime(benchmark.median)}</span>
</div>
<div class="metric">
<span class="metric-label">Std Dev</span>
<span class="metric-value">${formatTime(benchmark.std_dev)} <span class="metric-percent">(±${stdDevPercent}%)</span></span>
</div>
</div>
<div class="benchmark-bar">
<div class="benchmark-bar-fill" style="width: ${(benchmark.mean / slowest.mean * 100).toFixed(2)}%"></div>
</div>
</div>
`;
}
}).join('');
return `
<div class="benchmark-section">
<h2 class="section-header">${groupKey}</h2>
${sectionSummary}
<div class="benchmark-section-cards">
${cardsHtml}
</div>
</div>
`;
}).join('');
const searchInput = document.getElementById('benchmark-search');
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
const sections = document.querySelectorAll('.benchmark-section');
sections.forEach(section => {
const header = section.querySelector('.section-header').textContent.toLowerCase();
const subsections = section.querySelectorAll('.benchmark-subsection');
const cards = section.querySelectorAll('.benchmark-card');
let hasVisibleContent = false;
subsections.forEach(subsection => {
const subsectionHeader = subsection.querySelector('.subsection-header').textContent.toLowerCase();
const matchesSubsection = subsectionHeader.includes(query);
subsection.style.display = matchesSubsection ? 'block' : 'none';
if (matchesSubsection) hasVisibleContent = true;
});
cards.forEach(card => {
if (card.closest('.benchmark-subsection')) return;
const name = card.querySelector('.benchmark-name').textContent.toLowerCase();
const matchesCard = name.includes(query);
card.style.display = matchesCard ? 'block' : 'none';
if (matchesCard) hasVisibleContent = true;
});
section.style.display = (header.includes(query) || hasVisibleContent) ? 'block' : 'none';
});
});
document.getElementById('loading').style.display = 'none';
document.getElementById('benchmark-content').style.display = 'block';
}
function showError(message) {
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'block';
}
function renderTrendsChart(history, daysRange) {
const trendsSection = document.getElementById('historical-trends');
const canvas = document.getElementById('trends-chart');
if (!canvas || !history || !history.snapshots || history.snapshots.length < 2) {
if (trendsSection) trendsSection.style.display = 'none';
return;
}
trendsSection.style.display = 'block';
let snapshots = history.snapshots;
if (daysRange !== 'all') {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - parseInt(daysRange));
snapshots = snapshots.filter(s => new Date(s.timestamp) >= cutoffDate);
}
if (snapshots.length < 2) {
trendsSection.style.display = 'none';
return;
}
const fileTypes = ['Project Files', 'Class Files', 'Module Files', 'Form Files', 'Form Resources'];
const datasets = [];
const colors = {
'Project Files': '#3b82f6',
'Class Files': '#8b5cf6',
'Module Files': '#ec4899',
'Form Files': '#f59e0b',
'Form Resources': '#10b981'
};
fileTypes.forEach(fileType => {
const data = snapshots.map(snapshot => {
const benchmarks = snapshot.benchmarks.filter(b => {
const parsed = parseBenchmarkName(b.name);
return getFileTypeCategory(parsed.extension) === fileType;
});
if (benchmarks.length === 0) return null;
const avgTime = benchmarks.reduce((sum, b) => sum + b.mean, 0) / benchmarks.length;
return {
x: new Date(snapshot.timestamp),
y: avgTime / 1000000 };
}).filter(d => d !== null);
if (data.length > 0) {
datasets.push({
label: fileType,
data: data,
borderColor: colors[fileType],
backgroundColor: colors[fileType] + '20',
tension: 0.4,
borderWidth: 2,
pointRadius: 3,
pointHoverRadius: 5
});
}
});
if (window.trendsChartInstance) {
window.trendsChartInstance.destroy();
}
window.trendsChartSnapshots = snapshots;
const githubRepo = 'https://github.com/scriptandcompile/vb6parse';
const ctx = canvas.getContext('2d');
window.trendsChartInstance = new Chart(ctx, {
type: 'line',
data: { datasets },
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
onClick: (event, elements) => {
if (elements.length > 0) {
const index = elements[0].index;
const snapshot = window.trendsChartSnapshots[index];
if (snapshot && snapshot.commit_sha && snapshot.commit_sha !== 'unknown') {
window.open(`${githubRepo}/commit/${snapshot.commit_sha}`, '_blank');
}
}
},
plugins: {
legend: {
position: 'top',
labels: {
usePointStyle: true,
padding: 15
}
},
tooltip: {
callbacks: {
title: function(context) {
if (context.length > 0) {
const snapshot = window.trendsChartSnapshots[context[0].dataIndex];
if (snapshot) {
const date = new Date(snapshot.timestamp).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
return date;
}
}
return '';
},
beforeBody: function(context) {
if (context.length > 0) {
const snapshot = window.trendsChartSnapshots[context[0].dataIndex];
if (snapshot && snapshot.commit_sha && snapshot.commit_sha !== 'unknown') {
const commitShort = snapshot.commit_sha.substring(0, 8);
const commitMsg = snapshot.commit_message.substring(0, 60);
return [
`Commit: ${commitShort}`,
`${commitMsg}${snapshot.commit_message.length > 60 ? '...' : ''}`,
'' ];
}
}
return [];
},
label: function(context) {
return context.dataset.label + ': ' + context.parsed.y.toFixed(2) + ' ms';
},
footer: function(context) {
if (context.length > 0) {
const snapshot = window.trendsChartSnapshots[context[0].dataIndex];
if (snapshot && snapshot.commit_sha && snapshot.commit_sha !== 'unknown') {
return '(click to view on GitHub)';
}
}
return '';
}
}
}
},
scales: {
x: {
type: 'time',
time: {
unit: daysRange <= 30 ? 'day' : daysRange <= 90 ? 'week' : 'month',
displayFormats: {
day: 'MMM d',
week: 'MMM d',
month: 'MMM yyyy'
}
},
title: {
display: true,
text: 'Date'
}
},
y: {
beginAtZero: false,
title: {
display: true,
text: 'Average Time (ms)'
},
ticks: {
callback: function(value) {
return value.toFixed(2) + ' ms';
}
}
}
}
}
});
}
function setupTimeRangeSelector(history) {
const buttons = document.querySelectorAll('.time-range-btn');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
buttons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const range = btn.getAttribute('data-range');
renderTrendsChart(history, range);
});
});
}
loadBenchmarks();