Lekr0's picture
Add files using upload-large-folder tool
a227c91 verified
// SGLang Performance Dashboard Application
const GITHUB_REPO = 'sgl-project/sglang';
const WORKFLOW_NAME = 'nightly-test-nvidia.yml';
const ARTIFACT_PREFIX = 'consolidated-metrics-';
// Chart instances (array for batch-separated charts)
let activeCharts = [];
// Data storage
let allMetricsData = [];
let currentModel = null;
let currentMetricType = 'throughput'; // throughput, latency, ttft, inputThroughput
// Metric type definitions
const metricTypes = {
// Text/VLM metrics
throughput: { label: 'Overall Throughput', unit: 'tokens/sec', field: 'throughput', type: 'text' },
outputThroughput: { label: 'Output Throughput', unit: 'tokens/sec', field: 'outputThroughput', type: 'text' },
inputThroughput: { label: 'Input Throughput', unit: 'tokens/sec', field: 'inputThroughput', type: 'text' },
latency: { label: 'Latency', unit: 'ms', field: 'latency', type: 'text' },
ttft: { label: 'Time to First Token', unit: 'ms', field: 'ttft', type: 'text' },
accLength: { label: 'Accept Length', unit: 'tokens', field: 'accLength', filterInvalid: true, type: 'text' },
// Diffusion metrics
e2eMs: { label: 'End-to-End Time', unit: 'ms', field: 'e2e_ms', type: 'diffusion' },
avgDenoiseMs: { label: 'Avg Denoise Time', unit: 'ms', field: 'avg_denoise_ms', type: 'diffusion' },
medianDenoiseMs: { label: 'Median Denoise Time', unit: 'ms', field: 'median_denoise_ms', type: 'diffusion' }
};
// Chart.js default configuration for dark theme
Chart.defaults.color = '#94a3b8';
Chart.defaults.borderColor = '#1e293b';
const chartColors = [
'#22d3ee', '#34d399', '#fbbf24', '#f87171', '#a78bfa',
'#67e8f9', '#6ee7b7', '#fcd34d', '#fca5a5', '#c4b5fd'
];
// Initialize the dashboard
async function init() {
try {
await loadData();
document.getElementById('loading').style.display = 'none';
document.getElementById('content').style.display = 'block';
populateFilters();
updateStats();
updateCharts();
updateRunsTable();
} catch (error) {
console.error('Failed to initialize dashboard:', error);
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'block';
document.getElementById('error-message').textContent = error.message;
}
}
// Load data from local server API or GitHub
async function loadData() {
// Try local server API first (if running server.py)
try {
const response = await fetch('/api/metrics', { headers: getAuthHeaders() });
if (response.ok) {
const data = await response.json();
if (data.length > 0 && data[0].results && data[0].results.length > 0) {
allMetricsData = data;
console.log(`Loaded ${data.length} records from local API`);
allMetricsData.sort((a, b) => new Date(b.run_date) - new Date(a.run_date));
return;
}
}
} catch (error) {
console.log('Local API not available, trying GitHub API');
}
// Try to load from GitHub API
const runs = await fetchWorkflowRuns();
const metricsPromises = runs.map(run => fetchMetricsForRun(run));
const results = await Promise.allSettled(metricsPromises);
allMetricsData = results
.filter(r => r.status === 'fulfilled' && r.value !== null)
.map(r => r.value);
if (allMetricsData.length === 0) {
throw new Error('No metrics data available. Please run the server.py with --fetch-on-start to fetch data from GitHub.');
}
// Sort by date descending
allMetricsData.sort((a, b) => new Date(b.run_date) - new Date(a.run_date));
}
// Fetch workflow runs from GitHub API
async function fetchWorkflowRuns() {
const response = await fetch(
`https://api.github.com/repos/${GITHUB_REPO}/actions/workflows/${WORKFLOW_NAME}/runs?status=completed&per_page=30`,
{
headers: {
'Accept': 'application/vnd.github.v3+json'
}
}
);
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
const data = await response.json();
return data.workflow_runs || [];
}
// Fetch metrics artifact for a specific run
async function fetchMetricsForRun(run) {
try {
// Get artifacts for this run
const artifactsResponse = await fetch(
`https://api.github.com/repos/${GITHUB_REPO}/actions/runs/${run.id}/artifacts`,
{
headers: {
'Accept': 'application/vnd.github.v3+json'
}
}
);
if (!artifactsResponse.ok) return null;
const artifactsData = await artifactsResponse.json();
const metricsArtifact = artifactsData.artifacts.find(
a => a.name.startsWith(ARTIFACT_PREFIX)
);
if (!metricsArtifact) return null;
// Note: GitHub API doesn't allow direct artifact download without authentication
// For public access, we would need to use a proxy or pre-process the data
// For now, return run metadata - in production, use a backend to fetch artifacts
return {
run_id: run.id.toString(),
run_date: run.created_at,
commit_sha: run.head_sha,
branch: run.head_branch,
artifact_id: metricsArtifact.id,
results: [] // Would be populated from artifact content
};
} catch (error) {
console.warn(`Failed to fetch metrics for run ${run.id}:`, error);
return null;
}
}
// Helper function to detect if result is diffusion type
function isDiffusionResult(result) {
return result.test_type === 'diffusion' || (result.tests && !result.benchmarks);
}
// Populate filter dropdowns
function populateFilters() {
const gpuConfigs = new Set();
const models = new Set();
const testNames = new Set(); // For diffusion tests
const batchSizes = new Set();
const ioLengths = new Set();
allMetricsData.forEach(run => {
run.results.forEach(result => {
gpuConfigs.add(result.gpu_config);
// Handle diffusion results
if (isDiffusionResult(result)) {
models.add(result.test_suite || 'diffusion');
if (result.tests) {
result.tests.forEach(test => {
testNames.add(test.test_name);
});
}
}
// Handle text/VLM results
else {
models.add(result.model);
// Try new structure first (benchmarks_by_io_len), fall back to flat benchmarks
if (result.benchmarks_by_io_len) {
Object.entries(result.benchmarks_by_io_len).forEach(([ioKey, ioData]) => {
ioLengths.add(ioKey);
ioData.benchmarks.forEach(bench => {
batchSizes.add(bench.batch_size);
});
});
} else if (result.benchmarks) {
result.benchmarks.forEach(bench => {
batchSizes.add(bench.batch_size);
if (bench.input_len && bench.output_len) {
ioLengths.add(`${bench.input_len}_${bench.output_len}`);
}
});
}
}
});
});
// No "all" option for GPU and Model - populate with first value selected
const gpuArray = Array.from(gpuConfigs).sort();
const modelArray = Array.from(models).sort();
populateSelectNoAll('gpu-filter', gpuArray);
populateSelectNoAll('model-filter', modelArray);
populateSelect('batch-filter', Array.from(batchSizes).sort((a, b) => a - b));
populateSelectWithLabels('io-len-filter', sortIoLengths(Array.from(ioLengths)), formatIoLenLabel);
// Set initial values (first option)
if (gpuArray.length > 0) {
document.getElementById('gpu-filter').value = gpuArray[0];
}
if (modelArray.length > 0) {
document.getElementById('model-filter').value = modelArray[0];
currentModel = modelArray[0];
}
// Update variants based on selected model
updateVariantFilter();
// Update IO length filter based on selected GPU/model
updateIoLenFilter();
// Create metric type tabs
createMetricTabs();
}
// Format input/output length key for display
function formatIoLenLabel(ioKey) {
if (!ioKey) return 'Unknown';
const parts = ioKey.split('_');
if (parts.length === 2) {
return `In: ${parts[0]}, Out: ${parts[1]}`;
}
return ioKey;
}
// Sort IO length keys numerically (by input length, then output length)
function sortIoLengths(ioLengths) {
return ioLengths.filter(key => key && key.includes('_')).sort((a, b) => {
const [aIn, aOut] = a.split('_').map(Number);
const [bIn, bOut] = b.split('_').map(Number);
if (isNaN(aIn) || isNaN(bIn)) return 0;
return (aIn - bIn) || (aOut - bOut);
});
}
// Populate select with custom label formatting
function populateSelectWithLabels(selectId, options, labelFormatter) {
const select = document.getElementById(selectId);
options.forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.textContent = labelFormatter ? labelFormatter(option) : option;
select.appendChild(opt);
});
}
// Update IO length filter based on selected GPU and model
function updateIoLenFilter() {
const gpuFilterEl = document.getElementById('gpu-filter');
const modelFilterEl = document.getElementById('model-filter');
const ioLenSelect = document.getElementById('io-len-filter');
if (!gpuFilterEl || !modelFilterEl || !ioLenSelect) return;
const gpuFilter = gpuFilterEl.value;
const modelFilter = modelFilterEl.value;
const ioLengths = new Set();
allMetricsData.forEach(run => {
run.results.forEach(result => {
if (result.gpu_config === gpuFilter && result.model === modelFilter) {
if (result.benchmarks_by_io_len) {
Object.keys(result.benchmarks_by_io_len).forEach(ioKey => {
ioLengths.add(ioKey);
});
} else if (result.benchmarks) {
result.benchmarks.forEach(bench => {
if (bench.input_len && bench.output_len) {
ioLengths.add(`${bench.input_len}_${bench.output_len}`);
}
});
}
}
});
});
const ioLenArray = sortIoLengths(Array.from(ioLengths));
const currentIoLen = ioLenSelect.value;
// Clear and repopulate
ioLenSelect.innerHTML = '<option value="all">All Lengths</option>';
ioLenArray.forEach(ioLen => {
const opt = document.createElement('option');
opt.value = ioLen;
opt.textContent = formatIoLenLabel(ioLen);
ioLenSelect.appendChild(opt);
});
// Try to restore previous selection if still valid
if (ioLenArray.includes(currentIoLen)) {
ioLenSelect.value = currentIoLen;
} else {
ioLenSelect.value = 'all';
}
}
// Update variant filter based on selected GPU and model
function updateVariantFilter() {
const gpuFilter = document.getElementById('gpu-filter').value;
const modelFilter = document.getElementById('model-filter').value;
const variants = new Set();
allMetricsData.forEach(run => {
run.results.forEach(result => {
if (result.gpu_config === gpuFilter && result.model === modelFilter) {
// Use 'default' for null/undefined variants
variants.add(result.variant || 'default');
}
});
});
const variantArray = Array.from(variants).sort();
const variantSelect = document.getElementById('variant-filter');
const currentVariant = variantSelect.value;
// Clear and repopulate
variantSelect.innerHTML = '<option value="all">All Variants</option>';
variantArray.forEach(variant => {
const opt = document.createElement('option');
opt.value = variant;
opt.textContent = variant;
variantSelect.appendChild(opt);
});
// Try to restore previous selection if still valid
if (variantArray.includes(currentVariant)) {
variantSelect.value = currentVariant;
} else {
variantSelect.value = 'all';
}
}
function populateSelect(selectId, options) {
const select = document.getElementById(selectId);
options.forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.textContent = option;
select.appendChild(opt);
});
}
function populateSelectNoAll(selectId, options) {
const select = document.getElementById(selectId);
// Remove the "all" option if present
while (select.options.length > 0) {
select.remove(0);
}
options.forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.textContent = option;
select.appendChild(opt);
});
}
function createMetricTabs() {
const tabsContainer = document.getElementById('metric-tabs');
tabsContainer.innerHTML = '';
// Detect if current data is diffusion or text
const isDiffusion = detectCurrentDataType() === 'diffusion';
const dataType = isDiffusion ? 'diffusion' : 'text';
// Filter metrics based on data type
const relevantMetrics = Object.entries(metricTypes).filter(([key, metric]) =>
metric.type === dataType
);
relevantMetrics.forEach(([key, metric], index) => {
const tab = document.createElement('div');
tab.className = index === 0 ? 'tab active' : 'tab';
tab.textContent = metric.label;
tab.dataset.metric = key;
tab.onclick = () => selectMetricTab(key, tab);
tabsContainer.appendChild(tab);
});
// Set initial metric type
if (relevantMetrics.length > 0) {
currentMetricType = relevantMetrics[0][0];
}
}
function detectCurrentDataType() {
// Check if currently selected model/GPU config has diffusion data
const gpuFilter = document.getElementById('gpu-filter')?.value;
const modelFilter = currentModel;
if (!gpuFilter || !modelFilter) return 'text';
for (const run of allMetricsData) {
for (const result of run.results) {
if (result.gpu_config === gpuFilter) {
const resultModel = result.test_suite || result.model;
if (resultModel === modelFilter && isDiffusionResult(result)) {
return 'diffusion';
}
}
}
}
return 'text';
}
function selectMetricTab(metricKey, tabElement) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tabElement.classList.add('active');
currentMetricType = metricKey;
// Update chart title
const metric = metricTypes[metricKey];
document.getElementById('metric-title').textContent = `${metric.label} (${metric.unit})`;
updateCharts();
}
// Handle model filter dropdown change
function handleModelFilterChange(model) {
currentModel = model;
// Update variant filter based on new model selection
updateVariantFilter();
// Update IO length filter based on new model selection
updateIoLenFilter();
// Recreate metric tabs in case data type changed (text vs diffusion)
createMetricTabs();
updateCharts();
}
// Handle GPU filter change
function handleGpuFilterChange() {
// Update variant filter based on new GPU selection
updateVariantFilter();
// Update IO length filter based on new GPU selection
updateIoLenFilter();
// Recreate metric tabs in case data type changed (text vs diffusion)
createMetricTabs();
updateCharts();
}
// Update summary stats
function updateStats() {
const statsRow = document.getElementById('stats-row');
const latestRun = allMetricsData[0];
if (!latestRun) {
statsRow.innerHTML = '';
const noDataDiv = document.createElement('div');
noDataDiv.className = 'no-data';
noDataDiv.textContent = 'No data available';
statsRow.appendChild(noDataDiv);
return;
}
const totalModels = new Set(latestRun.results.map(r => r.model)).size;
const totalBenchmarks = latestRun.results.reduce((sum, r) => {
// Count benchmarks from either structure
if (r.benchmarks_by_io_len) {
return sum + Object.values(r.benchmarks_by_io_len).reduce(
(ioSum, ioData) => ioSum + ioData.benchmarks.length, 0
);
}
return sum + (r.benchmarks ? r.benchmarks.length : 0);
}, 0);
statsRow.innerHTML = ''; // Clear previous stats
const addStat = (label, value) => {
const card = document.createElement('div');
card.className = 'stat-card';
const labelEl = document.createElement('div');
labelEl.className = 'label';
labelEl.textContent = label;
const valueEl = document.createElement('div');
valueEl.className = 'value';
valueEl.textContent = value;
card.appendChild(labelEl);
card.appendChild(valueEl);
statsRow.appendChild(card);
};
addStat('Total Runs', allMetricsData.length);
addStat('Models Tested', totalModels);
addStat('Benchmarks', totalBenchmarks);
}
// Update charts based on current filters and selected metric type
function updateCharts() {
const gpuFilter = document.getElementById('gpu-filter').value;
const modelFilter = currentModel;
const variantFilter = document.getElementById('variant-filter').value;
const ioLenFilter = document.getElementById('io-len-filter').value;
const batchFilter = document.getElementById('batch-filter').value;
// Prepare data for charts - grouped by batch size
const chartDataByBatch = prepareChartDataByBatch(gpuFilter, modelFilter, variantFilter, ioLenFilter, batchFilter);
// Update chart for the selected metric type
updateMetricChart(chartDataByBatch, currentMetricType);
}
function prepareChartData(gpuFilter, modelFilter, variantFilter, ioLenFilter, batchFilter) {
const seriesMap = new Map();
allMetricsData.forEach(run => {
const runDate = new Date(run.run_date);
run.results.forEach(result => {
// Apply filters
if (result.gpu_config !== gpuFilter) return;
if (result.model !== modelFilter) return;
if (variantFilter !== 'all' && result.variant !== variantFilter) return;
// Helper function to process a benchmark entry
const processBenchmark = (bench, ioKey) => {
if (batchFilter !== 'all' && bench.batch_size !== parseInt(batchFilter)) return;
const ioLabel = ioKey ? `, ${formatIoLenLabel(ioKey)}` : '';
const seriesKey = `${result.model.split('/').pop()} (${result.variant}, BS=${bench.batch_size}${ioLabel})`;
if (!seriesMap.has(seriesKey)) {
seriesMap.set(seriesKey, {
label: seriesKey,
data: [],
model: result.model,
variant: result.variant,
batchSize: bench.batch_size,
ioKey: ioKey
});
}
seriesMap.get(seriesKey).data.push({
x: runDate,
throughput: bench.overall_throughput,
outputThroughput: bench.output_throughput,
latency: bench.latency_ms,
ttft: bench.ttft_ms,
inputThroughput: bench.input_throughput,
accLength: bench.acc_length,
runId: run.run_id
});
};
// Use benchmarks_by_io_len if available
if (result.benchmarks_by_io_len) {
Object.entries(result.benchmarks_by_io_len).forEach(([ioKey, ioData]) => {
if (ioLenFilter !== 'all' && ioKey !== ioLenFilter) return;
ioData.benchmarks.forEach(bench => processBenchmark(bench, ioKey));
});
} else if (result.benchmarks) {
result.benchmarks.forEach(bench => {
const benchIoKey = bench.input_len && bench.output_len
? `${bench.input_len}_${bench.output_len}`
: null;
if (ioLenFilter !== 'all' && benchIoKey !== ioLenFilter) return;
processBenchmark(bench, benchIoKey);
});
}
});
});
// Sort data points by date
seriesMap.forEach(series => {
series.data.sort((a, b) => a.x - b.x);
});
return Array.from(seriesMap.values());
}
// Prepare chart data grouped by batch size - each batch size is a separate series
function prepareChartDataByBatch(gpuFilter, modelFilter, variantFilter, ioLenFilter, batchFilter) {
const batchDataMap = new Map(); // batch_size -> Map of variant -> data
const testDataMap = new Map(); // For diffusion: test_name -> data
allMetricsData.forEach(run => {
const runDate = new Date(run.run_date);
run.results.forEach(result => {
// Apply filters - GPU and Model are required (no "all" option)
if (result.gpu_config !== gpuFilter) return;
// Handle diffusion results
if (isDiffusionResult(result)) {
const resultModel = result.test_suite || 'diffusion';
if (resultModel !== modelFilter) return;
if (result.tests) {
result.tests.forEach(test => {
const testName = test.test_name;
if (!testDataMap.has(testName)) {
testDataMap.set(testName, {
label: testName,
data: [],
model: resultModel,
testName: testName
});
}
testDataMap.get(testName).data.push({
x: runDate,
e2e_ms: test.e2e_ms,
avg_denoise_ms: test.avg_denoise_ms,
median_denoise_ms: test.median_denoise_ms,
runId: run.run_id
});
});
}
return;
}
// Handle text/VLM results
if (result.model !== modelFilter) return;
if (variantFilter !== 'all' && result.variant !== variantFilter) return;
// Use benchmarks_by_io_len if available, otherwise fall back to flat benchmarks
if (result.benchmarks_by_io_len) {
Object.entries(result.benchmarks_by_io_len).forEach(([ioKey, ioData]) => {
// Apply IO length filter
if (ioLenFilter !== 'all' && ioKey !== ioLenFilter) return;
ioData.benchmarks.forEach(bench => {
if (batchFilter !== 'all' && bench.batch_size !== parseInt(batchFilter)) return;
const batchSize = bench.batch_size;
const variantLabel = result.variant || 'default';
// Include IO length in series key when showing all lengths
const seriesKey = ioLenFilter === 'all'
? `${variantLabel} (${formatIoLenLabel(ioKey)})`
: variantLabel;
if (!batchDataMap.has(batchSize)) {
batchDataMap.set(batchSize, new Map());
}
const variantMap = batchDataMap.get(batchSize);
if (!variantMap.has(seriesKey)) {
variantMap.set(seriesKey, {
label: seriesKey,
data: [],
model: result.model,
variant: result.variant,
batchSize: batchSize,
ioKey: ioKey
});
}
variantMap.get(seriesKey).data.push({
x: runDate,
throughput: bench.overall_throughput,
outputThroughput: bench.output_throughput,
latency: bench.latency_ms,
ttft: bench.ttft_ms,
inputThroughput: bench.input_throughput,
accLength: bench.acc_length,
runId: run.run_id
});
});
});
} else if (result.benchmarks) {
// Fall back to flat benchmarks for backward compatibility
result.benchmarks.forEach(bench => {
// Apply IO length filter using flat structure
const benchIoKey = bench.input_len && bench.output_len
? `${bench.input_len}_${bench.output_len}`
: null;
if (ioLenFilter !== 'all' && benchIoKey !== ioLenFilter) return;
if (batchFilter !== 'all' && bench.batch_size !== parseInt(batchFilter)) return;
const batchSize = bench.batch_size;
const variantLabel = result.variant || 'default';
// Include IO length in series key when showing all lengths
const seriesKey = ioLenFilter === 'all' && benchIoKey
? `${variantLabel} (${formatIoLenLabel(benchIoKey)})`
: variantLabel;
if (!batchDataMap.has(batchSize)) {
batchDataMap.set(batchSize, new Map());
}
const variantMap = batchDataMap.get(batchSize);
if (!variantMap.has(seriesKey)) {
variantMap.set(seriesKey, {
label: seriesKey,
data: [],
model: result.model,
variant: result.variant,
batchSize: batchSize,
ioKey: benchIoKey
});
}
variantMap.get(seriesKey).data.push({
x: runDate,
throughput: bench.overall_throughput,
outputThroughput: bench.output_throughput,
latency: bench.latency_ms,
ttft: bench.ttft_ms,
inputThroughput: bench.input_throughput,
accLength: bench.acc_length,
runId: run.run_id
});
});
}
});
});
// Sort data points by date and convert to array format
const result = {};
// For diffusion data, use test names as "batch sizes"
if (testDataMap.size > 0) {
testDataMap.forEach((series, testName) => {
series.data.sort((a, b) => a.x - b.x);
result[testName] = [series]; // Each test is its own series
});
return result;
}
// For text/VLM data, use batch sizes
batchDataMap.forEach((variantMap, batchSize) => {
variantMap.forEach(series => {
series.data.sort((a, b) => a.x - b.x);
});
result[batchSize] = Array.from(variantMap.values());
});
return result;
}
// Unified chart update function for any metric type
function updateMetricChart(chartDataByBatch, metricType) {
const container = document.getElementById('charts-container');
container.innerHTML = '';
// Destroy existing charts
activeCharts.forEach(chart => chart.destroy());
activeCharts = [];
const metric = metricTypes[metricType];
const isDiffusion = metric.type === 'diffusion';
// For diffusion, keys are test names; for text, keys are batch sizes
const keys = Object.keys(chartDataByBatch);
if (!isDiffusion) {
keys.sort((a, b) => parseInt(a) - parseInt(b));
} else {
keys.sort(); // Alphabetical sort for test names
}
const batchSizes = keys; // Keep variable name for compatibility
if (batchSizes.length === 0) {
container.innerHTML = '<div class="no-data">No data available for the selected filters</div>';
return;
}
let hasAnyData = false;
batchSizes.forEach(batchSize => {
const chartData = chartDataByBatch[batchSize];
const ctx_datasets = chartData.map((series, index) => {
// Filter data points - for metrics like accLength, exclude invalid values (-1 or null)
let dataPoints = series.data.map(d => ({ x: d.x, y: d[metric.field] }));
if (metric.filterInvalid) {
dataPoints = dataPoints.filter(d => d.y != null && d.y !== -1 && d.y > 0);
}
return {
label: series.label,
data: dataPoints,
borderColor: chartColors[index % chartColors.length],
backgroundColor: chartColors[index % chartColors.length] + '20',
tension: 0.1,
fill: false
};
}).filter(dataset => dataset.data.length > 0); // Remove empty datasets
// Skip this batch size if no valid data
if (ctx_datasets.length === 0) {
return;
}
hasAnyData = true;
const chartWrapper = document.createElement('div');
chartWrapper.className = 'batch-chart-wrapper';
const title = document.createElement('div');
title.className = 'batch-chart-title';
// For diffusion, show test name; for text, show batch size
title.textContent = isDiffusion ? `Test: ${batchSize}` : `Batch Size: ${batchSize}`;
chartWrapper.appendChild(title);
const chartContainer = document.createElement('div');
chartContainer.className = 'chart-container';
const canvas = document.createElement('canvas');
chartContainer.appendChild(canvas);
chartWrapper.appendChild(chartContainer);
container.appendChild(chartWrapper);
const ctx = canvas.getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: { datasets: ctx_datasets },
options: getChartOptions(metric.unit)
});
activeCharts.push(chart);
});
// Show message if no valid data for this metric
if (!hasAnyData) {
container.innerHTML = `<div class="no-data">No valid ${metric.label.toLowerCase()} data available for the selected filters</div>`;
}
}
function getChartOptions(yAxisLabel) {
return {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: {
position: 'bottom',
labels: {
boxWidth: 12,
padding: 10,
font: { size: 11 }
}
},
tooltip: {
backgroundColor: '#1a2332',
borderColor: 'rgba(148, 163, 184, 0.1)',
borderWidth: 1,
titleFont: { size: 13, family: "'DM Sans', sans-serif" },
bodyFont: { size: 12, family: "'JetBrains Mono', monospace" },
padding: 14,
cornerRadius: 8
}
},
scales: {
x: {
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'MMM d'
}
},
grid: {
color: 'rgba(148, 163, 184, 0.06)'
}
},
y: {
title: {
display: true,
text: yAxisLabel
},
grid: {
color: 'rgba(148, 163, 184, 0.06)'
}
}
}
};
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Update runs table
function updateRunsTable() {
const tbody = document.getElementById('runs-table-body');
tbody.innerHTML = '';
allMetricsData.slice(0, 10).forEach(run => {
const models = new Set(run.results.map(r => r.model.split('/').pop()));
const date = new Date(run.run_date);
const row = document.createElement('tr');
// Create cells safely to prevent XSS
const dateCell = document.createElement('td');
dateCell.textContent = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
const runIdCell = document.createElement('td');
const runLink = document.createElement('a');
runLink.href = `https://github.com/${GITHUB_REPO}/actions/runs/${encodeURIComponent(run.run_id)}`;
runLink.target = '_blank';
runLink.className = 'run-link';
runLink.textContent = run.run_id;
runIdCell.appendChild(runLink);
const commitCell = document.createElement('td');
const commitCode = document.createElement('code');
commitCode.textContent = run.commit_sha.substring(0, 7);
commitCell.appendChild(commitCode);
const branchCell = document.createElement('td');
branchCell.textContent = run.branch;
const modelsCell = document.createElement('td');
Array.from(models).forEach((model, index) => {
if (index > 0) modelsCell.appendChild(document.createTextNode(' '));
const badge = document.createElement('span');
badge.className = 'model-badge';
badge.textContent = model;
modelsCell.appendChild(badge);
});
row.appendChild(dateCell);
row.appendChild(runIdCell);
row.appendChild(commitCell);
row.appendChild(branchCell);
row.appendChild(modelsCell);
tbody.appendChild(row);
});
}
// Refresh data
async function refreshData() {
document.getElementById('content').style.display = 'none';
document.getElementById('loading').style.display = 'flex';
await init();
}
// Format numbers for display
function formatNumber(num) {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k';
}
return num.toFixed(1);
}
// Authentication state
let authToken = sessionStorage.getItem('dashboard_auth_token') || null;
// Get auth headers for API requests
function getAuthHeaders() {
const headers = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
return headers;
}
// Check if server requires authentication and show/hide login accordingly
async function checkAuthAndInit() {
const loginOverlay = document.getElementById('login-overlay');
const dashboardContainer = document.getElementById('dashboard-container');
try {
const response = await fetch('/api/auth-check');
if (response.ok) {
const data = await response.json();
if (!data.auth_required) {
// No auth required - skip login, show dashboard directly
loginOverlay.style.display = 'none';
dashboardContainer.style.display = 'block';
init();
return;
}
}
} catch (e) {
// Server not available (e.g. static hosting) - skip login
loginOverlay.style.display = 'none';
dashboardContainer.style.display = 'block';
init();
return;
}
// Auth is required - check if we have a valid token from a previous session
if (authToken) {
try {
const testResponse = await fetch('/api/metrics', {
headers: getAuthHeaders()
});
if (testResponse.ok) {
loginOverlay.style.display = 'none';
dashboardContainer.style.display = 'block';
init();
return;
}
} catch (e) {
// Token invalid or expired
}
// Clear invalid token
authToken = null;
sessionStorage.removeItem('dashboard_auth_token');
}
// Show login form
loginOverlay.style.display = 'flex';
dashboardContainer.style.display = 'none';
}
// Handle login form submission
async function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('login-username').value;
const password = document.getElementById('login-password').value;
const errorEl = document.getElementById('login-error');
const loginBtn = document.getElementById('login-btn');
errorEl.textContent = '';
loginBtn.disabled = true;
loginBtn.textContent = 'Signing in...';
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok && data.token) {
authToken = data.token;
sessionStorage.setItem('dashboard_auth_token', authToken);
document.getElementById('login-overlay').style.display = 'none';
document.getElementById('dashboard-container').style.display = 'block';
init();
} else {
errorEl.textContent = data.error || 'Invalid username or password';
}
} catch (e) {
errorEl.textContent = 'Unable to connect to server';
} finally {
loginBtn.disabled = false;
loginBtn.textContent = 'Sign In';
}
return false;
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', checkAuthAndInit);