Created
November 7, 2025 20:05
-
-
Save TomAugspurger/c32becfcba4d2cbcd7678a2d6579e371 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Benchmark Results Explorer</title> | |
| <link href="https://unpkg.com/[email protected]/dist/css/tabulator.min.css" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| margin: 0; | |
| padding: 20px; | |
| background: #f5f5f5; | |
| overflow: hidden; | |
| } | |
| .container { | |
| max-width: 100%; | |
| margin: 0 auto; | |
| display: flex; | |
| gap: 0; | |
| height: calc(100vh - 40px); | |
| } | |
| .table-container { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 8px 0 0 8px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| flex: 1; | |
| min-width: 200px; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .resize-handle { | |
| width: 8px; | |
| background: #e0e0e0; | |
| cursor: col-resize; | |
| position: relative; | |
| flex-shrink: 0; | |
| transition: background 0.2s; | |
| } | |
| .resize-handle:hover { | |
| background: #bdbdbd; | |
| } | |
| .resize-handle:active { | |
| background: #9e9e9e; | |
| } | |
| .resize-handle::after { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 2px; | |
| height: 40px; | |
| background: #999; | |
| border-radius: 1px; | |
| } | |
| body.resizing { | |
| cursor: col-resize !important; | |
| user-select: none !important; | |
| } | |
| body.resizing * { | |
| cursor: col-resize !important; | |
| } | |
| .details-panel { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 0 8px 8px 0; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| width: 500px; | |
| min-width: 300px; | |
| max-width: 1600px; | |
| overflow-y: auto; | |
| flex-shrink: 0; | |
| } | |
| .details-panel h2 { | |
| margin-top: 0; | |
| font-size: 1.2em; | |
| border-bottom: 2px solid #007bff; | |
| padding-bottom: 10px; | |
| } | |
| .details-panel h3 { | |
| font-size: 1em; | |
| margin-top: 20px; | |
| color: #666; | |
| } | |
| .details-panel pre { | |
| background: #f8f9fa; | |
| padding: 10px; | |
| border-radius: 4px; | |
| overflow-x: auto; | |
| font-size: 0.85em; | |
| } | |
| .details-panel .empty-state { | |
| color: #999; | |
| font-style: italic; | |
| text-align: center; | |
| padding: 40px; | |
| } | |
| .info-grid { | |
| display: grid; | |
| grid-template-columns: auto 1fr; | |
| gap: 8px; | |
| font-size: 0.9em; | |
| } | |
| .info-grid dt { | |
| font-weight: 600; | |
| color: #666; | |
| } | |
| .info-grid dd { | |
| margin: 0; | |
| } | |
| #file-input { | |
| margin-bottom: 20px; | |
| } | |
| .tabulator-row.tabulator-selected { | |
| background-color: #e3f2fd !important; | |
| } | |
| #table:focus { | |
| outline: 2px solid #007bff; | |
| outline-offset: -2px; | |
| } | |
| kbd { | |
| background-color: #f4f4f4; | |
| border: 1px solid #ccc; | |
| border-radius: 3px; | |
| padding: 2px 6px; | |
| font-family: monospace; | |
| font-size: 0.9em; | |
| box-shadow: 0 1px 0 rgba(0,0,0,0.2); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="table-container"> | |
| <h1>Benchmark Results Explorer</h1> | |
| <div style="margin-bottom: 10px; color: #666; font-size: 0.9em;"> | |
| Use <kbd>j</kbd>/<kbd>k</kbd> or <kbd>↑</kbd>/<kbd>↓</kbd> to navigate rows | |
| </div> | |
| <input type="file" id="file-input" accept=".jsonl,.json" multiple /> | |
| <div id="file-status" style="margin-top: 8px; margin-bottom: 8px; color: #28a745; font-size: 0.9em;"></div> | |
| <div id="table" tabindex="0" style="flex: 1; overflow: auto;"></div> | |
| </div> | |
| <div class="resize-handle" id="resize-handle"></div> | |
| <div class="details-panel" id="details-panel"> | |
| <div class="empty-state">Select a row to view details</div> | |
| </div> | |
| </div> | |
| <script src="https://unpkg.com/[email protected]/dist/js/tabulator.min.js"></script> | |
| <script> | |
| let table; | |
| let runsData = []; | |
| // Parse JSONL file | |
| function parseJSONL(text) { | |
| return text.trim().split('\n') | |
| .filter(line => line.trim()) | |
| .map(line => JSON.parse(line)); | |
| } | |
| // Flatten run data into table rows | |
| function flattenRunData(runs) { | |
| const rows = []; | |
| runs.forEach((run, runIndex) => { | |
| for (const [queryId, records] of Object.entries(run.records)) { | |
| records.forEach(record => { | |
| rows.push({ | |
| runIndex: runIndex, | |
| run: `Run ${runIndex + 1}`, | |
| queryId: parseInt(queryId), | |
| iteration: record.iteration, | |
| duration: record.duration, | |
| executor: run.executor, | |
| cluster: run.cluster, | |
| scale_factor: run.scale_factor, | |
| timestamp: run.timestamp, | |
| _runData: run, | |
| _recordData: record | |
| }); | |
| }); | |
| } | |
| }); | |
| return rows; | |
| } | |
| // Show details for selected row | |
| function showDetails(row) { | |
| const data = row.getData(); | |
| const run = data._runData; | |
| const record = data._recordData; | |
| const panel = document.getElementById('details-panel'); | |
| let html = ` | |
| <h2>Query ${data.queryId} - Iteration ${data.iteration}</h2> | |
| `; | |
| // Show query plans first if available | |
| const plan = run.plans?.find(p => p.query === data.queryId); | |
| if (plan) { | |
| if (plan.physical_plan) { | |
| html += `<h3>Physical Plan</h3><pre>${plan.physical_plan}</pre>`; | |
| } | |
| if (plan.logical_plan) { | |
| html += `<h3>Logical Plan</h3><pre>${plan.logical_plan}</pre>`; | |
| } | |
| } | |
| html += ` | |
| <h3>Execution</h3> | |
| <dl class="info-grid"> | |
| <dt>Duration:</dt><dd>${data.duration.toFixed(4)}s</dd> | |
| <dt>Query ID:</dt><dd>${data.queryId}</dd> | |
| <dt>Iteration:</dt><dd>${data.iteration}</dd> | |
| </dl> | |
| <h3>Run Configuration</h3> | |
| <dl class="info-grid"> | |
| <dt>Executor:</dt><dd>${run.executor}</dd> | |
| <dt>Runtime:</dt><dd>${run.runtime || 'N/A'}</dd> | |
| <dt>Cluster:</dt><dd>${run.cluster || 'N/A'}</dd> | |
| <dt>Scale Factor:</dt><dd>${run.scale_factor}</dd> | |
| <dt>Timestamp:</dt><dd>${new Date(run.timestamp).toLocaleString()}</dd> | |
| <dt>Workers:</dt><dd>${run.n_workers}</dd> | |
| <dt>Threads:</dt><dd>${run.threads}</dd> | |
| </dl> | |
| <h3>Software Versions</h3> | |
| <dl class="info-grid"> | |
| <dt>Polars:</dt><dd>${run.versions.polars}</dd> | |
| <dt>Python:</dt><dd>${run.versions.python}</dd> | |
| <dt>cudf_polars:</dt><dd>${typeof run.versions.cudf_polars === 'object' ? run.versions.cudf_polars.version : run.versions.cudf_polars}</dd> | |
| </dl> | |
| <h3>Hardware</h3> | |
| <dl class="info-grid"> | |
| <dt>GPUs:</dt><dd>${run.hardware.gpus.length}</dd> | |
| ${run.hardware.gpus[0] ? `<dt>GPU Type:</dt><dd>${run.hardware.gpus[0].name}</dd>` : ''} | |
| </dl> | |
| `; | |
| // Show shuffle stats if available | |
| if (record.shuffle_stats) { | |
| html += `<h3>Shuffle Statistics</h3><pre>${JSON.stringify(record.shuffle_stats, null, 2)}</pre>`; | |
| } | |
| panel.innerHTML = html; | |
| } | |
| // Initialize table | |
| function initTable(data) { | |
| table = new Tabulator("#table", { | |
| data: data, | |
| layout: "fitData", | |
| selectable: 1, | |
| height: "100%", | |
| columns: [ | |
| {title: "Run", field: "run", width: 100, sorter: "number"}, | |
| {title: "Query ID", field: "queryId", width: 100, sorter: "number"}, | |
| {title: "Iteration", field: "iteration", width: 100, sorter: "number"}, | |
| {title: "Duration (s)", field: "duration", width: 120, sorter: "number", | |
| formatter: (cell) => cell.getValue().toFixed(4)}, | |
| {title: "Executor", field: "executor", width: 120}, | |
| {title: "Cluster", field: "cluster", width: 120}, | |
| {title: "Scale Factor", field: "scale_factor", width: 120}, | |
| {title: "Timestamp", field: "timestamp", width: 200, | |
| formatter: (cell) => new Date(cell.getValue()).toLocaleString()} | |
| ], | |
| initialSort: [ | |
| {column: "runIndex", dir: "asc"}, | |
| {column: "queryId", dir: "asc"}, | |
| {column: "iteration", dir: "asc"} | |
| ] | |
| }); | |
| // Handle row selection | |
| table.on("rowClick", function(e, row){ | |
| showDetails(row); | |
| }); | |
| // Keyboard navigation | |
| table.on("rowSelected", function(row){ | |
| showDetails(row); | |
| }); | |
| // Select first row after table is built | |
| table.on("tableBuilt", function(){ | |
| const rows = table.getRows(); | |
| if (rows.length > 0) { | |
| table.selectRow(rows[0]); | |
| } | |
| }); | |
| } | |
| // File input handler - supports multiple files | |
| document.getElementById('file-input').addEventListener('change', function(e) { | |
| const files = Array.from(e.target.files); | |
| if (files.length === 0) return; | |
| // Read all files and concatenate them | |
| const readPromises = files.map(file => { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => resolve(e.target.result); | |
| reader.onerror = reject; | |
| reader.readAsText(file); | |
| }); | |
| }); | |
| Promise.all(readPromises) | |
| .then(contents => { | |
| try { | |
| // Concatenate all file contents | |
| const combinedContent = contents.join('\n'); | |
| runsData = parseJSONL(combinedContent); | |
| const flatData = flattenRunData(runsData); | |
| // Update status message | |
| const statusEl = document.getElementById('file-status'); | |
| const fileWord = files.length === 1 ? 'file' : 'files'; | |
| const runWord = runsData.length === 1 ? 'run' : 'runs'; | |
| statusEl.textContent = `✓ Loaded ${files.length} ${fileWord} (${runsData.length} ${runWord}, ${flatData.length} records)`; | |
| if (table) { | |
| table.replaceData(flatData); | |
| // Re-select first row after data reload | |
| setTimeout(() => { | |
| const rows = table.getRows(); | |
| if (rows.length > 0) { | |
| table.selectRow(rows[0]); | |
| } | |
| }, 0); | |
| } else { | |
| initTable(flatData); | |
| } | |
| } catch (error) { | |
| document.getElementById('file-status').textContent = ''; | |
| alert('Error parsing file(s): ' + error.message); | |
| } | |
| }) | |
| .catch(error => { | |
| document.getElementById('file-status').textContent = ''; | |
| alert('Error reading file(s): ' + error.message); | |
| }); | |
| }); | |
| // Vim-style keyboard navigation (j/k keys) | |
| document.addEventListener('keydown', function(e) { | |
| // Don't interfere with typing in input fields | |
| if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { | |
| return; | |
| } | |
| if (!table) return; | |
| const selectedRows = table.getSelectedRows(); | |
| if (selectedRows.length === 0) { | |
| // If no row is selected, select the first one | |
| const rows = table.getRows(); | |
| if (rows.length > 0) { | |
| table.selectRow(rows[0]); | |
| } | |
| return; | |
| } | |
| const currentRow = selectedRows[0]; | |
| let targetRow = null; | |
| if (e.key === 'j' || e.key === 'J') { | |
| // j = down | |
| e.preventDefault(); | |
| targetRow = table.getNextRow(currentRow); | |
| } else if (e.key === 'k' || e.key === 'K') { | |
| // k = up | |
| e.preventDefault(); | |
| targetRow = table.getPrevRow(currentRow); | |
| } else if (e.key === 'ArrowDown') { | |
| // Also support arrow keys if they work | |
| e.preventDefault(); | |
| targetRow = table.getNextRow(currentRow); | |
| } else if (e.key === 'ArrowUp') { | |
| e.preventDefault(); | |
| targetRow = table.getPrevRow(currentRow); | |
| } | |
| if (targetRow) { | |
| table.deselectRow(); | |
| table.selectRow(targetRow); | |
| // Scroll to the row to keep it visible | |
| targetRow.scrollTo(); | |
| } | |
| }); | |
| // Resizable panels | |
| (function() { | |
| const resizeHandle = document.getElementById('resize-handle'); | |
| const detailsPanel = document.getElementById('details-panel'); | |
| let isResizing = false; | |
| resizeHandle.addEventListener('mousedown', function(e) { | |
| isResizing = true; | |
| document.body.classList.add('resizing'); | |
| e.preventDefault(); | |
| }); | |
| document.addEventListener('mousemove', function(e) { | |
| if (!isResizing) return; | |
| const containerWidth = document.querySelector('.container').offsetWidth; | |
| const newWidth = containerWidth - e.clientX + 20; // 20px for padding | |
| // Enforce min and max width constraints | |
| if (newWidth >= 300 && newWidth <= 1600) { | |
| detailsPanel.style.width = newWidth + 'px'; | |
| } | |
| }); | |
| document.addEventListener('mouseup', function() { | |
| if (isResizing) { | |
| isResizing = false; | |
| document.body.classList.remove('resizing'); | |
| } | |
| }); | |
| // Also handle mouse leaving the window | |
| document.addEventListener('mouseleave', function() { | |
| if (isResizing) { | |
| isResizing = false; | |
| document.body.classList.remove('resizing'); | |
| } | |
| }); | |
| })(); | |
| // Optional: Auto-load a default file if hosted with the data | |
| // fetch('pdsh_results.jsonl') | |
| // .then(r => r.text()) | |
| // .then(text => { | |
| // runsData = parseJSONL(text); | |
| // initTable(flattenRunData(runsData)); | |
| // }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment