From fd06f88862102b5b30712b67cb9512ce5ed14554 Mon Sep 17 00:00:00 2001 From: Soowan Park <144195149+soowanpNV@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:32:48 +0900 Subject: [PATCH] Render benchmark labels as text and verify the Chart.js asset --- .github/workflows/tests.yml | 10 +++ docs/index.html | 2 +- docs/js/dashboard.js | 86 ++++++++++++++------- tests/dashboard.test.mjs | 147 ++++++++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+), 29 deletions(-) create mode 100644 tests/dashboard.test.mjs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 12eebee..e2b5845 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,3 +29,13 @@ jobs: - name: Run unit tests run: uv run python -m unittest discover -s tests + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Run dashboard tests + run: | + node --check docs/js/dashboard.js + node --test tests/dashboard.test.mjs diff --git a/docs/index.html b/docs/index.html index fddd15a..5c0afa2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -9,7 +9,7 @@ Isaac Lab Presets Performance Dashboard - + diff --git a/docs/js/dashboard.js b/docs/js/dashboard.js index 835bdf7..5a010c5 100644 --- a/docs/js/dashboard.js +++ b/docs/js/dashboard.js @@ -4,6 +4,13 @@ const $ = (s) => document.querySelector(s); const $$ = (s) => [...document.querySelectorAll(s)]; + function textElement(tag, className, value) { + const element = document.createElement(tag); + element.className = className; + element.textContent = String(value); + return element; + } + const PRESET_COLORS = [ "#00d4ff", "#ff3e96", "#00ff88", "#ff6b35", "#ffea00", "#7c4dff", "#00bcd4", "#e040fb", "#69f0ae", "#ff5722", @@ -677,7 +684,10 @@ if (pendingMemCharts.length > 0) { const memHeading = document.createElement("div"); memHeading.className = "scaling-category-heading"; - memHeading.innerHTML = `GPU Memory Usage${gpuTotalMemGb ? `Total VRAM: ${formatNumber(gpuTotalMemGb)} GB` : ""}`; + memHeading.append( + textElement("span", "", "GPU Memory Usage"), + textElement("span", "scaling-category-sub", gpuTotalMemGb ? `Total VRAM: ${formatNumber(gpuTotalMemGb)} GB` : ""), + ); container.appendChild(memHeading); for (const { rawTask, wfShort, memData: md, memPresetLabels: mpl } of pendingMemCharts) { @@ -805,7 +815,9 @@ for (const item of items) { const div = document.createElement("div"); div.className = "info-env-item"; - div.innerHTML = `${item.label}${item.value}`; + const value = textElement("span", "value", item.value); + value.title = String(item.value); + div.append(textElement("span", "label", item.label), value); container.appendChild(div); } } @@ -927,9 +939,7 @@ if (warnings.length > 0) { const banner = document.createElement("div"); banner.className = "warning-banner"; - banner.innerHTML = warnings - .map((w) => `
\u26A0 ${w}
`) - .join(""); + banner.append(...warnings); container.appendChild(banner); } @@ -1080,6 +1090,16 @@ function detectPresetMismatches(run) { const warnings = []; + function addWarning(preset, kind, configured, actual) { + const line = document.createElement("div"); + line.className = "warning-line"; + line.append( + "\u26A0 Preset ", textElement("strong", "", preset), + ` configured ${kind} as `, textElement("strong", "", configured), + ", but ", textElement("strong", "", actual), " was actually used.", + ); + warnings.push(line); + } for (const entry of run.entries) { if (!entry.preset || isHiddenPreset(entry.preset)) continue; const p = parsePreset(entry.preset); @@ -1090,14 +1110,10 @@ const presetPhys = p.physics.toLowerCase(); if (actualPhys && presetPhys && actualPhys !== presetPhys) { - warnings.push( - `Preset ${entry.preset} configured physics as ${p.physics}, but ${entry.actual_physics} was actually used.` - ); + addWarning(entry.preset, "physics", p.physics, entry.actual_physics); } if (actualRend && p.renderer && !rendererMatches(p.renderer, actualRend)) { - warnings.push( - `Preset ${entry.preset} configured renderer as ${p.renderer}, but ${actualRend} was actually used.` - ); + addWarning(entry.preset, "renderer", p.renderer, actualRend); } } return warnings; @@ -1178,15 +1194,21 @@ const mi = presetMaxIter[item.preset]; const cmd = buildShellCmd(workflow, rawTask, numEnvs, rawPreset, nf, mi); const vram = vramValues[item.preset]; - let vramStr = ""; + const rankLabel = textElement("span", "rank-label", label); + if (numEnvs) { + rankLabel.append(" ", textElement("span", "rank-envs", `(${numEnvs} envs)`)); + } if (vram != null) { const pct = gpuTotalGb ? ((vram / gpuTotalGb) * 100).toFixed(1) : null; - vramStr = pct - ? ` ${formatNumber(vram)} GB / ${pct}% VRAM` - : ` ${formatNumber(vram)} GB VRAM`; + const vramText = pct + ? `${formatNumber(vram)} GB / ${pct}% VRAM` + : `${formatNumber(vram)} GB VRAM`; + rankLabel.append(" ", textElement("span", "rank-vram", vramText)); } - li.innerHTML = `${label}${numEnvs ? ` (${numEnvs} envs)` : ""}${vramStr}${formatNumber(item.value)}`; - li.querySelector(".copy-cmd-btn").addEventListener("click", (e) => { + const copyButton = textElement("button", "copy-cmd-btn", "\uD83D\uDCCB"); + copyButton.title = "Copy run command"; + li.append(copyButton, rankLabel, textElement("span", "rank-value", formatNumber(item.value))); + copyButton.addEventListener("click", (e) => { e.stopPropagation(); const btn = e.currentTarget; function onSuccess() { @@ -1347,11 +1369,14 @@ for (const preset of currentOrder) { const th = document.createElement("th"); const p = parsePreset(preset); - th.innerHTML = `
- ${p.physics} - ${p.renderer} - ${p.datatype} -
`; + const header = document.createElement("div"); + header.className = "preset-header-cell"; + header.append( + textElement("span", "preset-part physics", p.physics), + textElement("span", "preset-part renderer", p.renderer), + textElement("span", "preset-part datatype", p.datatype), + ); + th.appendChild(header); headerRow.appendChild(th); } thead.appendChild(headerRow); @@ -1436,7 +1461,9 @@ for (const item of items) { const div = document.createElement("div"); div.className = "version-item"; - div.innerHTML = `${item.label}${item.value}`; + const value = textElement("span", "version-value", item.value); + value.title = String(item.value); + div.append(textElement("span", "version-label", item.label), value); bar.appendChild(div); } } @@ -1805,10 +1832,10 @@ const p = parsePreset(entry.preset); const div = document.createElement("div"); div.className = "summary-metric"; - div.innerHTML = ` -
${p.physics} / ${p.renderer} / ${p.datatype}
-
${formatNumber(val)}
- `; + div.append( + textElement("div", "label", `${p.physics} / ${p.renderer} / ${p.datatype}`), + textElement("div", "value", formatNumber(val)), + ); content.appendChild(div); } } @@ -2075,7 +2102,10 @@ const info = allWfTasks[col]; const th = document.createElement("th"); const wfShort = info.workflow.replace("benchmark_", "").replace("_train", ""); - th.innerHTML = `
${wfShort}${info.task}
`; + const header = document.createElement("div"); + header.className = "status-col-header"; + header.append(textElement("span", "", wfShort), textElement("span", "status-task-name", info.task)); + th.appendChild(header); hRow.appendChild(th); } thead.appendChild(hRow); diff --git a/tests/dashboard.test.mjs b/tests/dashboard.test.mjs new file mode 100644 index 0000000..987f0c9 --- /dev/null +++ b/tests/dashboard.test.mjs @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA Corporation +// SPDX-License-Identifier: Apache-2.0 + +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { test } from 'node:test'; +import vm from 'node:vm'; + +// A DOM recorder for the synchronous renderers. It deliberately does not parse +// HTML: benchmark values must reach text nodes, with presentation nodes retained. +function loadDashboard() { + const htmlWrites = []; + class Element { + constructor(tagName) { + this.tagName = tagName; + this.children = []; + this.className = ''; + this.style = {}; + this.listeners = {}; + this.classList = { + add: (...names) => { this.className += ' ' + names.join(' '); }, + toggle: () => {}, + }; + } + set textContent(value) { this.children = []; this.text = String(value); } + get textContent() { return (this.text ?? '') + this.children.map(child => child.textContent).join(''); } + set innerHTML(value) { this.children = []; this.text = ''; this.html = value; htmlWrites.push(value); } + get innerHTML() { return this.html ?? ''; } + appendChild(child) { this.children.push(child); return child; } + append(...children) { + for (const child of children) { + if (typeof child === 'string') { + const text = new Element('#text'); + text.textContent = child; + this.appendChild(text); + } else this.appendChild(child); + } + } + addEventListener(name, fn) { this.listeners[name] = fn; } + querySelectorAll(selector) { + const matches = node => selector.startsWith('.') + ? node.className.split(/\s+/).includes(selector.slice(1)) + : node.tagName === selector; + return this.children.flatMap(child => [ + ...(matches(child) ? [child] : []), ...child.querySelectorAll(selector), + ]); + } + querySelector(selector) { + return this.querySelectorAll(selector)[0] ?? null; + } + } + const roots = new Map(); + const document = { + createElement: tag => new Element(tag), + createTextNode: text => { const node = new Element('#text'); node.textContent = text; return node; }, + querySelector: selector => { + if (!roots.has(selector)) roots.set(selector, new Element('div')); + return roots.get(selector); + }, + }; + const context = vm.createContext({ document, URLSearchParams, + window: { location: { pathname: '/' } }, history: { replaceState() {} }, + }); + const source = readFileSync(new URL('../docs/js/dashboard.js', import.meta.url), 'utf8'); + const marker = ' /* ─── Init & Events ─── */'; + assert.ok(source.includes(marker)); + vm.runInContext(source.slice(0, source.indexOf(marker)) + ` + globalThis.dashboard = { state, renderInfo, renderVersionBar, detectPresetMismatches, + buildFullTable, buildFpsRankCard, showHistRunDetail, renderBenchmarkStatusGrid }; + })();`, context); + return { ...context.dashboard, document, htmlWrites }; +} + +const label = 'Team A & B "release"'; +const preset = `${label},Renderer,RGB`; +const run = { + commit_sha: 'abc123', commit_date: '2026-01-01', isaac_sim_version: label, + hostname: label, env_info: {}, entries: [{ preset, ci_job_id: '123', + actual_physics: 'Physics & Simulation', benchmarks: [{ + workflow: 'benchmark_non_rl', task: label, runtime: { FPS: 12 }, + }], + }], +}; + +test('environment values retain their literal text and titles', () => { + const d = loadDashboard(); + d.state.currentGpu = 'gpu'; + d.state.manifest = { datasets: [{ gpu_type: 'gpu', file: 'gpu' }] }; + d.state.datasets.gpu = { runs: [run] }; + d.renderInfo(); + const values = d.document.querySelector('#infoBenchmarkEnv').querySelectorAll('.value'); + assert.ok(values.some(value => value.textContent === label && value.title === label)); + assert.ok(!d.htmlWrites.some(html => html.includes(label))); +}); + +test('version values retain their literal text and titles', () => { + const d = loadDashboard(); + d.renderVersionBar(run, '#versions'); + const values = d.document.querySelector('#versions').querySelectorAll('.version-value'); + assert.ok(values.some(value => value.textContent === label && value.title === label)); +}); + +test('mismatch warnings keep emphasis around literal preset and engine names', () => { + const d = loadDashboard(); + const warning = d.detectPresetMismatches(run)[0]; + assert.equal(warning.textContent, `⚠ Preset ${preset} configured physics as ${label}, but Physics & Simulation was actually used.`); + assert.equal(warning.querySelectorAll('strong').length, 3); + assert.equal(warning.className, 'warning-line'); +}); + +test('table headings preserve the preset parts as styled text', () => { + const d = loadDashboard(); + const table = d.buildFullTable('FPS', ['FPS'], [preset], { FPS: { [preset]: 12 } }, true); + const physics = table.querySelector('.physics'); + assert.equal(physics?.textContent, label); + assert.equal(table.querySelector('.renderer')?.textContent, 'Renderer'); + assert.equal(table.querySelector('.datatype')?.textContent, 'RGB'); +}); + +test('ranking text retains environment and memory details plus the copy action', () => { + const d = loadDashboard(); + const card = d.buildFpsRankCard('FPS', [preset], { + FPS: { [preset]: 12 }, 'GPU Memory Used': { [preset]: 2 }, + }, 'benchmark_non_rl', { [preset]: 512 }, {}, {}, {}, {}, {}, run); + assert.ok(card.querySelector('.rank-label')?.textContent.includes(label)); + assert.equal(card.querySelector('.rank-envs')?.textContent, '(512 envs)'); + assert.equal(card.querySelector('.rank-vram')?.textContent, '2.00 GB VRAM'); + assert.equal(typeof card.querySelector('.copy-cmd-btn')?.listeners.click, 'function'); +}); + +test('history summary values and labels are text', () => { + const d = loadDashboard(); + d.state.histWorkflow = 'benchmark_non_rl'; + d.state.histMetric = 'FPS'; + d.showHistRunDetail({ runs: [run] }, 0); + const summary = d.document.querySelector('#histSummaryContent'); + assert.equal(summary.querySelector('.label')?.textContent, `${label} / Renderer / RGB`); + assert.equal(summary.querySelector('.value')?.textContent, '12.00'); +}); + +test('status columns retain literal task names and their header structure', () => { + const d = loadDashboard(); + d.renderBenchmarkStatusGrid({ runs: [run] }); + const grid = d.document.querySelector('#benchmarkStatusGrid'); + assert.equal(grid.querySelector('.status-task-name')?.textContent, label); + assert.equal(grid.querySelector('.status-col-header')?.children.length, 2); +});