| |
|
|
| const GITHUB_REPO = 'sgl-project/sglang'; |
| const WORKFLOW_NAME = 'nightly-test-nvidia.yml'; |
| const ARTIFACT_PREFIX = 'consolidated-metrics-'; |
|
|
| |
| let activeCharts = []; |
|
|
| |
| let allMetricsData = []; |
| let currentModel = null; |
| let currentMetricType = 'throughput'; |
|
|
| |
| const metricTypes = { |
| |
| 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' }, |
| |
| 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.defaults.color = '#94a3b8'; |
| Chart.defaults.borderColor = '#1e293b'; |
|
|
| const chartColors = [ |
| '#22d3ee', '#34d399', '#fbbf24', '#f87171', '#a78bfa', |
| '#67e8f9', '#6ee7b7', '#fcd34d', '#fca5a5', '#c4b5fd' |
| ]; |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| async function loadData() { |
| |
| 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'); |
| } |
|
|
| |
| 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.'); |
| } |
|
|
| |
| allMetricsData.sort((a, b) => new Date(b.run_date) - new Date(a.run_date)); |
| } |
|
|
| |
| 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 || []; |
| } |
|
|
| |
| async function fetchMetricsForRun(run) { |
| try { |
| |
| 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; |
|
|
| |
| |
| |
| 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: [] |
| }; |
| } catch (error) { |
| console.warn(`Failed to fetch metrics for run ${run.id}:`, error); |
| return null; |
| } |
| } |
|
|
| |
| function isDiffusionResult(result) { |
| return result.test_type === 'diffusion' || (result.tests && !result.benchmarks); |
| } |
|
|
| |
| function populateFilters() { |
| const gpuConfigs = new Set(); |
| const models = new Set(); |
| const testNames = new Set(); |
| const batchSizes = new Set(); |
| const ioLengths = new Set(); |
|
|
| allMetricsData.forEach(run => { |
| run.results.forEach(result => { |
| gpuConfigs.add(result.gpu_config); |
|
|
| |
| if (isDiffusionResult(result)) { |
| models.add(result.test_suite || 'diffusion'); |
| if (result.tests) { |
| result.tests.forEach(test => { |
| testNames.add(test.test_name); |
| }); |
| } |
| } |
| |
| else { |
| models.add(result.model); |
| |
| 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}`); |
| } |
| }); |
| } |
| } |
| }); |
| }); |
|
|
| |
| 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); |
|
|
| |
| 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]; |
| } |
|
|
| |
| updateVariantFilter(); |
| |
| updateIoLenFilter(); |
|
|
| |
| createMetricTabs(); |
| } |
|
|
| |
| function formatIoLenLabel(ioKey) { |
| if (!ioKey) return 'Unknown'; |
| const parts = ioKey.split('_'); |
| if (parts.length === 2) { |
| return `In: ${parts[0]}, Out: ${parts[1]}`; |
| } |
| return ioKey; |
| } |
|
|
| |
| 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); |
| }); |
| } |
|
|
| |
| 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); |
| }); |
| } |
|
|
| |
| 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; |
|
|
| |
| 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); |
| }); |
|
|
| |
| if (ioLenArray.includes(currentIoLen)) { |
| ioLenSelect.value = currentIoLen; |
| } else { |
| ioLenSelect.value = 'all'; |
| } |
| } |
|
|
| |
| 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) { |
| |
| variants.add(result.variant || 'default'); |
| } |
| }); |
| }); |
|
|
| const variantArray = Array.from(variants).sort(); |
| const variantSelect = document.getElementById('variant-filter'); |
| const currentVariant = variantSelect.value; |
|
|
| |
| 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); |
| }); |
|
|
| |
| 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); |
| |
| 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 = ''; |
|
|
| |
| const isDiffusion = detectCurrentDataType() === 'diffusion'; |
| const dataType = isDiffusion ? 'diffusion' : 'text'; |
|
|
| |
| 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); |
| }); |
|
|
| |
| if (relevantMetrics.length > 0) { |
| currentMetricType = relevantMetrics[0][0]; |
| } |
| } |
|
|
| function detectCurrentDataType() { |
| |
| 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; |
|
|
| |
| const metric = metricTypes[metricKey]; |
| document.getElementById('metric-title').textContent = `${metric.label} (${metric.unit})`; |
|
|
| updateCharts(); |
| } |
|
|
| |
| function handleModelFilterChange(model) { |
| currentModel = model; |
| |
| updateVariantFilter(); |
| |
| updateIoLenFilter(); |
| |
| createMetricTabs(); |
| updateCharts(); |
| } |
|
|
| |
| function handleGpuFilterChange() { |
| |
| updateVariantFilter(); |
| |
| updateIoLenFilter(); |
| |
| createMetricTabs(); |
| updateCharts(); |
| } |
|
|
| |
| 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) => { |
| |
| 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 = ''; |
|
|
| 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); |
| } |
|
|
| |
| 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; |
|
|
| |
| const chartDataByBatch = prepareChartDataByBatch(gpuFilter, modelFilter, variantFilter, ioLenFilter, batchFilter); |
|
|
| |
| 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 => { |
| |
| if (result.gpu_config !== gpuFilter) return; |
| if (result.model !== modelFilter) return; |
| if (variantFilter !== 'all' && result.variant !== variantFilter) return; |
|
|
| |
| 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 |
| }); |
| }; |
|
|
| |
| 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); |
| }); |
| } |
| }); |
| }); |
|
|
| |
| seriesMap.forEach(series => { |
| series.data.sort((a, b) => a.x - b.x); |
| }); |
|
|
| return Array.from(seriesMap.values()); |
| } |
|
|
| |
| function prepareChartDataByBatch(gpuFilter, modelFilter, variantFilter, ioLenFilter, batchFilter) { |
| const batchDataMap = new Map(); |
| const testDataMap = new Map(); |
|
|
| allMetricsData.forEach(run => { |
| const runDate = new Date(run.run_date); |
|
|
| run.results.forEach(result => { |
| |
| if (result.gpu_config !== gpuFilter) return; |
|
|
| |
| 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; |
| } |
|
|
| |
| if (result.model !== modelFilter) return; |
| if (variantFilter !== 'all' && result.variant !== variantFilter) return; |
|
|
| |
| 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 => { |
| if (batchFilter !== 'all' && bench.batch_size !== parseInt(batchFilter)) return; |
|
|
| const batchSize = bench.batch_size; |
| const variantLabel = result.variant || 'default'; |
| |
| 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) { |
| |
| 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; |
| if (batchFilter !== 'all' && bench.batch_size !== parseInt(batchFilter)) return; |
|
|
| const batchSize = bench.batch_size; |
| const variantLabel = result.variant || 'default'; |
| |
| 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 |
| }); |
| }); |
| } |
| }); |
| }); |
|
|
| |
| const result = {}; |
|
|
| |
| if (testDataMap.size > 0) { |
| testDataMap.forEach((series, testName) => { |
| series.data.sort((a, b) => a.x - b.x); |
| result[testName] = [series]; |
| }); |
| return result; |
| } |
|
|
| |
| batchDataMap.forEach((variantMap, batchSize) => { |
| variantMap.forEach(series => { |
| series.data.sort((a, b) => a.x - b.x); |
| }); |
| result[batchSize] = Array.from(variantMap.values()); |
| }); |
|
|
| return result; |
| } |
|
|
| |
| function updateMetricChart(chartDataByBatch, metricType) { |
| const container = document.getElementById('charts-container'); |
| container.innerHTML = ''; |
|
|
| |
| activeCharts.forEach(chart => chart.destroy()); |
| activeCharts = []; |
|
|
| const metric = metricTypes[metricType]; |
| const isDiffusion = metric.type === 'diffusion'; |
|
|
| |
| const keys = Object.keys(chartDataByBatch); |
| if (!isDiffusion) { |
| keys.sort((a, b) => parseInt(a) - parseInt(b)); |
| } else { |
| keys.sort(); |
| } |
| const batchSizes = keys; |
|
|
| 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) => { |
| |
| 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); |
|
|
| |
| 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'; |
| |
| 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); |
| }); |
|
|
| |
| 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)' |
| } |
| } |
| } |
| }; |
| } |
|
|
| |
| function escapeHtml(text) { |
| const div = document.createElement('div'); |
| div.textContent = text; |
| return div.innerHTML; |
| } |
|
|
| |
| 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'); |
|
|
| |
| 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); |
| }); |
| } |
|
|
| |
| async function refreshData() { |
| document.getElementById('content').style.display = 'none'; |
| document.getElementById('loading').style.display = 'flex'; |
| await init(); |
| } |
|
|
| |
| function formatNumber(num) { |
| if (num >= 1000) { |
| return (num / 1000).toFixed(1) + 'k'; |
| } |
| return num.toFixed(1); |
| } |
|
|
| |
| let authToken = sessionStorage.getItem('dashboard_auth_token') || null; |
|
|
| |
| function getAuthHeaders() { |
| const headers = {}; |
| if (authToken) { |
| headers['Authorization'] = `Bearer ${authToken}`; |
| } |
| return headers; |
| } |
|
|
| |
| 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) { |
| |
| loginOverlay.style.display = 'none'; |
| dashboardContainer.style.display = 'block'; |
| init(); |
| return; |
| } |
| } |
| } catch (e) { |
| |
| loginOverlay.style.display = 'none'; |
| dashboardContainer.style.display = 'block'; |
| init(); |
| return; |
| } |
|
|
| |
| 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) { |
| |
| } |
| |
| authToken = null; |
| sessionStorage.removeItem('dashboard_auth_token'); |
| } |
|
|
| |
| loginOverlay.style.display = 'flex'; |
| dashboardContainer.style.display = 'none'; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', checkAuthAndInit); |
|
|