Wildminder's picture
Update index.html
1f7611c verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sampler/Scheduler Performance Matrix</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.heatmap-cell {
transition: all 0.2s ease;
position: relative;
}
.heatmap-cell:hover {
transform: scale(1.05);
box-shadow: 0 0 10px rgba(0,0,0,0.2);
z-index: 10;
}
.heatmap-cell:hover::after {
content: attr(data-value);
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 20;
}
.sticky-header {
position: sticky;
top: 0;
z-index: 20;
background-color: white;
}
.sticky-col {
position: sticky;
left: 0;
z-index: 10;
background-color: white;
}
.scroll-container {
max-height: 80vh;
overflow: auto;
}
.color-scale {
background: linear-gradient(90deg, #f0fff4, #9ae6b4, #48bb78, #f6e05e, #f6ad55, #f56565);
}
</style>
</head>
<body class="bg-gray-50 font-sans">
<div class="container mx-auto px-4 py-8">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
<div>
<h1 class="text-3xl font-bold text-gray-800 mb-2">Sampler/Scheduler Performance Matrix</h1>
<p class="text-gray-600">Visualizing execution speed across different combinations</p>
</div>
<div class="flex items-center gap-4">
<div class="relative">
<input type="text" id="searchInput" placeholder="Search samplers..."
class="pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">Performance:</span>
<div class="flex items-center">
<span class="text-xs mr-1">Fast</span>
<div class="w-24 h-4 rounded-full color-scale"></div>
<span class="text-xs ml-1">Slow</span>
</div>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-md overflow-hidden">
<div class="p-4 border-b">
<div class="flex justify-between items-center">
<h2 class="text-xl font-semibold text-gray-700">Performance Metrics (iterations/sec)</h2>
<div class="flex items-center gap-2">
<button id="toggleHeatmap" class="px-3 py-1 bg-blue-100 text-blue-600 rounded-md text-sm hover:bg-blue-200 transition">
<i class="fas fa-fire mr-1"></i> Toggle Heatmap
</button>
</div>
</div>
</div>
<div class="scroll-container">
<table class="w-full">
<thead>
<tr class="sticky-header">
<th class="sticky-col p-3 text-left font-medium text-gray-700 bg-white border-r min-w-[180px]">Sampler \ Scheduler</th>
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">beta</th>
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">beta57</th>
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">ddim_uniform</th>
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">exponential</th>
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">kl_optimal</th>
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">karras</th>
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">linear_quadratic</th>
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">normal</th>
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">sgm_uniform</th>
<th class="p-3 text-center font-medium text-gray-700 bg-white border-b">simple</th>
</tr>
</thead>
<tbody id="matrixBody">
<!-- Table content will be generated by JavaScript -->
</tbody>
</table>
</div>
</div>
<div class="mt-8 bg-white rounded-xl shadow-md p-6">
<h2 class="text-xl font-semibold text-gray-700 mb-4">Performance Insights</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-blue-50 p-4 rounded-lg">
<div class="flex items-center mb-2">
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3">
<i class="fas fa-trophy text-blue-500"></i>
</div>
<h3 class="font-medium text-gray-700">Top Performer</h3>
</div>
<p class="text-gray-600 text-sm" id="topPerformer">Loading...</p>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<div class="flex items-center mb-2">
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3">
<i class="fas fa-star text-green-500"></i>
</div>
<h3 class="font-medium text-gray-700">Most Consistent</h3>
</div>
<p class="text-gray-600 text-sm" id="mostConsistent">Loading...</p>
</div>
<div class="bg-red-50 p-4 rounded-lg">
<div class="flex items-center mb-2">
<div class="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center mr-3">
<i class="fas fa-snail text-red-500"></i>
</div>
<h3 class="font-medium text-gray-700">Slowest</h3>
</div>
<p class="text-gray-600 text-sm" id="slowest">Loading...</p>
</div>
</div>
</div>
</div>
<script>
// Global variables
let originalData = {};
let normalizedData = {};
let sortedSamplers = [];
let sortedSchedulers = [];
let minValue = Infinity; // Min normalized value (excluding base)
let maxValue = -Infinity; // Max normalized value (excluding base)
let heatmapEnabled = true;
const BASE_SAMPLER = 'euler';
const BASE_SCHEDULER = 'simple';
let baseNormalizedValue = 1.0; // Expected normalized value for base
// --- Text File Loading Helper ---
async function fetchTextList(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status} for ${url}`);
}
const text = await response.text();
return text.split('\n').map(s => s.trim()).filter(Boolean); // Split by newline, trim whitespace, remove empty lines
} catch (error) {
console.error(`Failed to fetch list from ${url}:`, error);
return []; // Return empty array on error
}
}
// --- Normalization Function ---
function normalizeData(rawData) {
if (!rawData || typeof rawData !== 'object' || Object.keys(rawData).length === 0) {
console.error("Cannot normalize empty or invalid data.");
return {};
}
const baseValue = rawData[BASE_SAMPLER]?.[BASE_SCHEDULER];
if (baseValue === undefined || baseValue === null || baseValue <= 0) {
console.error(`Base value for normalization (${BASE_SAMPLER}/${BASE_SCHEDULER}) not found or invalid. Skipping normalization.`);
return JSON.parse(JSON.stringify(rawData)); // Return a copy
}
console.log(`Normalizing data using base ${BASE_SAMPLER}/${BASE_SCHEDULER} = ${baseValue}`);
baseNormalizedValue = 1.0; // By definition
const normalized = {};
minValue = Infinity;
maxValue = -Infinity;
for (const sampler in rawData) {
normalized[sampler] = {};
for (const scheduler in rawData[sampler]) {
const originalValue = rawData[sampler][scheduler];
if (originalValue !== undefined && originalValue !== null) {
const normValue = originalValue / baseValue;
normalized[sampler][scheduler] = normValue;
// Update min/max EXCLUDING the base value itself for color scaling
if (sampler !== BASE_SAMPLER || scheduler !== BASE_SCHEDULER) {
if (normValue < minValue) minValue = normValue;
// --- FIXED ERROR HERE ---
if (normValue > maxValue) maxValue = normValue; // Corrected from 'value' to 'normValue'
}
} else {
normalized[sampler][scheduler] = undefined;
}
}
}
// Handle edge case where only the base value exists or all others are invalid
if (!isFinite(minValue) || !isFinite(maxValue) || minValue >= maxValue) { // Check if range is valid
console.warn("Could not determine valid min/max range for heatmap scaling (excluding base). Using default range [0.5, 1.5] for colors.");
// Set a reasonable default range relative to the base value
minValue = Math.min(0.5, baseNormalizedValue * 0.5);
maxValue = Math.max(1.5, baseNormalizedValue * 1.5);
} else {
// Ensure the base value is conceptually within the range for smoother color transitions near white
// Widen range slightly if necessary to avoid extreme colors right next to white base
minValue = Math.min(minValue, baseNormalizedValue * 0.9); // e.g., ensure range includes at least 0.9
maxValue = Math.max(maxValue, baseNormalizedValue * 1.1); // e.g., ensure range includes at least 1.1
}
console.log(`Normalized range for heatmap (excluding base): [${minValue.toFixed(2)}, ${maxValue.toFixed(2)}]`);
return normalized;
}
// Calculate color based on NORMALIZED value with more steps and base case
function getColor(value, sampler, scheduler) {
// --- Special case for base combination ---
if (sampler === BASE_SAMPLER && scheduler === BASE_SCHEDULER) {
return 'bg-white border border-gray-400'; // White background with a border for visibility
}
if (!heatmapEnabled || value === undefined || value === null || !isFinite(value)) {
return 'bg-gray-100'; // Gray for missing/invalid data or when heatmap off
}
if (!isFinite(minValue) || !isFinite(maxValue) || minValue >= maxValue) { // Check maxValue > minValue
return 'bg-white'; // Default if range is invalid
}
// Normalize the value within the calculated range (excluding base)
const range = maxValue - minValue;
const normalizedForColor = range > 0 ? (value - minValue) / range : 0.5; // Avoid division by zero, default to mid
// Refined color scale (adjust thresholds and colors as needed)
// Lower value (closer to minValue) = faster/greener
// Higher value (closer to maxValue) = slower/redder
if (normalizedForColor <= 0.05) return 'bg-emerald-400'; // Fastest 5%
if (normalizedForColor <= 0.15) return 'bg-emerald-300';
if (normalizedForColor <= 0.30) return 'bg-green-300';
if (normalizedForColor <= 0.45) return 'bg-lime-300'; // Added lime
if (normalizedForColor <= 0.60) return 'bg-yellow-300'; // Mid-range
if (normalizedForColor <= 0.75) return 'bg-amber-300'; // Added amber
if (normalizedForColor <= 0.85) return 'bg-orange-400';
if (normalizedForColor <= 0.95) return 'bg-red-400';
return 'bg-red-500'; // Slowest 5%
}
// Generate table rows using NORMALIZED data and SORTED keys
function generateTable() {
const matrixBody = document.getElementById('matrixBody');
if (!matrixBody || Object.keys(normalizedData).length === 0 || sortedSamplers.length === 0 || sortedSchedulers.length === 0) {
console.warn("Cannot generate table: Missing data or sorting lists.");
if (matrixBody) matrixBody.innerHTML = '<tr><td colspan="11" class="text-center p-4 text-gray-500">Data or sorting order missing.</td></tr>';
return;
}
matrixBody.innerHTML = ''; // Clear previous content
// --- Update table header based on sortedSchedulers ---
const headerRow = document.querySelector('thead tr');
// Clear existing scheduler headers (skip the first 'Sampler \ Scheduler' th)
while (headerRow.children.length > 1) {
headerRow.removeChild(headerRow.lastChild);
}
// Add new scheduler headers in the correct order
sortedSchedulers.forEach(scheduler => {
const th = document.createElement('th');
th.className = "p-3 text-center font-medium text-gray-700 bg-white border-b min-w-[100px]";
th.textContent = scheduler;
headerRow.appendChild(th);
});
// --- Generate table body rows based on sortedSamplers ---
sortedSamplers.forEach(sampler => {
// Only add row if the sampler exists in the data
if (!normalizedData[sampler]) {
console.warn(`Sampler "${sampler}" from samplers.txt not found in data.`);
return; // Skip this sampler
}
const row = document.createElement('tr');
row.className = 'sampler-row hover:bg-gray-50';
// Sampler name cell
const samplerCell = document.createElement('td');
samplerCell.className = 'sticky-col p-3 text-left font-medium text-gray-700 bg-white border-r';
samplerCell.textContent = sampler;
row.appendChild(samplerCell);
// Scheduler cells - iterate in the specified order
sortedSchedulers.forEach(scheduler => {
const value = normalizedData[sampler]?.[scheduler]; // Use normalized value, check existence
const cell = document.createElement('td');
const displayValue = (value !== undefined && value !== null && isFinite(value)) ? value.toFixed(2) : '-';
// Get color, passing sampler and scheduler for base case check
cell.className = `heatmap-cell p-3 text-center border-b ${getColor(value, sampler, scheduler)}`;
cell.textContent = displayValue;
// Tooltip
if (value !== undefined && value !== null && isFinite(value)) {
const originalValue = originalData[sampler]?.[scheduler];
const originalDisplay = originalValue !== undefined ? ` (${originalValue.toFixed(2)}s)` : '';
cell.setAttribute('data-value', `${sampler} + ${scheduler}: ${displayValue} (norm)${originalDisplay}`);
} else {
cell.setAttribute('data-value', `${sampler} + ${scheduler}: N/A`);
}
row.appendChild(cell);
});
matrixBody.appendChild(row);
});
}
// Find performance insights using NORMALIZED data
function calculateInsights() {
if (Object.keys(normalizedData).length === 0) return;
const allCombinations = [];
const consistency = {};
// Use sorted keys to iterate if necessary, though iterating over normalizedData keys is fine here
for (const sampler in normalizedData) {
// Skip samplers not in the desired list if strict adherence is needed elsewhere
// if (!sortedSamplers.includes(sampler)) continue;
let samplerValues = [];
let samplerSum = 0;
let samplerCount = 0;
for (const scheduler in normalizedData[sampler]) {
// Skip schedulers not in the desired list if strict adherence is needed elsewhere
// if (!sortedSchedulers.includes(scheduler)) continue;
const value = normalizedData[sampler][scheduler];
if (value === undefined || value === null || !isFinite(value)) continue;
allCombinations.push({ sampler, scheduler, value });
// Exclude base value from consistency calculation if desired (usually not needed for CV)
// if (sampler !== BASE_SAMPLER || scheduler !== BASE_SCHEDULER) {
samplerValues.push(value);
samplerSum += value;
samplerCount++;
// }
}
if (samplerCount > 0) {
const mean = samplerSum / samplerCount;
let variance = 0;
samplerValues.forEach(v => { variance += Math.pow(v - mean, 2); });
variance /= samplerCount; // Or samplerCount - 1 for sample variance
const stdDev = Math.sqrt(variance);
const cv = (mean > 0) ? (stdDev / mean) : (stdDev === 0 ? 0 : Infinity);
consistency[sampler] = { stdDev, mean, cv };
}
}
// Sort for Top 5 Fastest (lowest normalized value)
allCombinations.sort((a, b) => a.value - b.value);
const top5Fastest = allCombinations.slice(0, 5);
// Sort for Top 5 Slowest (highest normalized value)
const top5Slowest = allCombinations.slice(-5).reverse();
// Find most consistent sampler (lowest CV based on normalized data)
let mostConsistent = { cv: Infinity, sampler: '', stdDev: NaN };
for (const sampler in consistency) {
if (consistency[sampler].cv < mostConsistent.cv) {
mostConsistent = {
cv: consistency[sampler].cv,
sampler: sampler,
stdDev: consistency[sampler].stdDev
};
}
}
// --- Update DOM ---
const topPerformerEl = document.getElementById('topPerformer');
const mostConsistentEl = document.getElementById('mostConsistent');
const slowestEl = document.getElementById('slowest');
if (topPerformerEl) {
topPerformerEl.innerHTML = top5Fastest.map(item =>
`<span>${item.sampler} + ${item.scheduler}: <b>${item.value.toFixed(2)}</b></span>`
).join('<br>');
if (top5Fastest.length === 0) topPerformerEl.textContent = 'N/A';
}
if (mostConsistentEl) {
mostConsistentEl.textContent = isFinite(mostConsistent.cv)
? `${mostConsistent.sampler} (Norm. CV: ${mostConsistent.cv.toFixed(2)})` // Simplified output
: 'N/A';
}
if (slowestEl) {
slowestEl.innerHTML = top5Slowest.map(item =>
`<span>${item.sampler} + ${item.scheduler}: <b>${item.value.toFixed(2)}</b></span>`
).join('<br>');
if (top5Slowest.length === 0) slowestEl.textContent = 'N/A';
}
}
// Toggle heatmap colors
function toggleHeatmap() {
heatmapEnabled = !heatmapEnabled;
const cells = document.querySelectorAll('.heatmap-cell');
cells.forEach(cell => {
// Get the associated sampler/scheduler to check for base case
// This requires adding data attributes to the cell in generateTable or parsing the tooltip
// Simpler approach: rely on the initial class setting in generateTable
// We just need to re-apply the correct color based on the current heatmapEnabled state
const valueText = cell.textContent;
let value = NaN;
if (valueText && valueText !== '-' && !isNaN(parseFloat(valueText))) {
value = parseFloat(valueText);
}
// Find sampler/scheduler - this is inefficient, better to add data-* attributes
let sampler = '';
let scheduler = '';
const row = cell.closest('tr');
if (row) {
sampler = row.cells[0]?.textContent || '';
const colIndex = Array.from(cell.parentNode.children).indexOf(cell);
scheduler = document.querySelector(`thead th:nth-child(${colIndex + 1})`)?.textContent || '';
}
// Remove all potential background color classes first
cell.classList.remove(
'bg-emerald-400', 'bg-emerald-300', 'bg-green-300', 'bg-lime-300',
'bg-yellow-300', 'bg-amber-300', 'bg-orange-400', 'bg-red-400', 'bg-red-500',
'bg-gray-100', 'bg-white', 'border', 'border-gray-400' // Remove border too
);
// Apply the correct class based on state and value
cell.classList.add(getColor(value, sampler, scheduler));
});
// Update button appearance (remains the same)
const toggleButton = document.getElementById('toggleHeatmap');
if (toggleButton) {
if (heatmapEnabled) {
toggleButton.innerHTML = '<i class="fas fa-ban mr-1"></i> Disable Heatmap';
toggleButton.classList.remove('bg-red-100', 'text-red-600', 'hover:bg-red-200');
toggleButton.classList.add('bg-blue-100', 'text-blue-600', 'hover:bg-blue-200');
} else {
toggleButton.innerHTML = '<i class="fas fa-fire mr-1"></i> Enable Heatmap';
toggleButton.classList.remove('bg-blue-100', 'text-blue-600', 'hover:bg-blue-200');
toggleButton.classList.add('bg-red-100', 'text-red-600', 'hover:bg-red-200');
}
}
}
// Search functionality (no change needed)
function setupSearch() {
const searchInput = document.getElementById('searchInput');
if (!searchInput) return;
searchInput.addEventListener('input', () => {
const searchTerm = searchInput.value.toLowerCase().trim();
const rows = document.querySelectorAll('#matrixBody .sampler-row');
rows.forEach(row => {
const samplerNameCell = row.cells[0];
if (samplerNameCell) {
const samplerName = samplerNameCell.textContent.toLowerCase();
if (samplerName.includes(searchTerm)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
}
});
});
}
// Fetch data and initialize the page
async function initializeApp() {
try {
// --- Fetch sorting lists and data CONCURRENTLY ---
const [samplerList, schedulerList, jsonDataResponse] = await Promise.all([
fetchTextList('samplers.txt'),
fetchTextList('schedulers.txt'),
fetch('performance_data.json')
]);
sortedSamplers = samplerList;
sortedSchedulers = schedulerList;
if (!jsonDataResponse.ok) {
throw new Error(`HTTP error! status: ${jsonDataResponse.status} for performance_data.json`);
}
originalData = await jsonDataResponse.json();
// --- Validate fetched data ---
if (sortedSamplers.length === 0) console.warn("Sampler sorting list is empty.");
if (sortedSchedulers.length === 0) console.warn("Scheduler sorting list is empty.");
if (Object.keys(originalData).length === 0) {
throw new Error("Fetched performance data is empty.");
}
// --- Normalize Data ---
normalizedData = normalizeData(originalData);
if (Object.keys(normalizedData).length === 0) {
// normalizeData might return original data if base fails, re-check
if (Object.keys(originalData).length > 0) {
console.warn("Normalization failed, using original data structure but results might be unexpected.");
normalizedData = originalData; // Fallback for display structure
} else {
throw new Error("Normalization failed and original data is empty.");
}
}
// Calculate insights using NORMALIZED data
calculateInsights();
// Generate the table content using NORMALIZED data and SORTED lists
generateTable();
// Set up search functionality
setupSearch();
// Attach event listener for the heatmap toggle button
const toggleButton = document.getElementById('toggleHeatmap');
if (toggleButton) {
if (heatmapEnabled) {
toggleButton.innerHTML = '<i class="fas fa-ban mr-1"></i> Disable Heatmap';
toggleButton.classList.add('bg-blue-100', 'text-blue-600', 'hover:bg-blue-200');
} else {
toggleButton.innerHTML = '<i class="fas fa-fire mr-1"></i> Enable Heatmap';
toggleButton.classList.add('bg-red-100', 'text-red-600', 'hover:bg-red-200');
}
toggleButton.addEventListener('click', toggleHeatmap);
}
} catch (error) {
console.error("Failed to initialize app:", error);
const matrixBody = document.getElementById('matrixBody');
if (matrixBody) {
matrixBody.innerHTML = `<tr><td colspan="11" class="text-center p-4 text-red-500">Error initializing: ${error.message}</td></tr>`;
}
document.getElementById('topPerformer').textContent = 'Error';
document.getElementById('mostConsistent').textContent = 'Error';
document.getElementById('slowest').textContent = 'Error';
}
}
// Initialize when the DOM is ready
document.addEventListener('DOMContentLoaded', initializeApp);
</script>
</body>
</html>