Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<title>Isaac Lab Presets Performance Dashboard</title>
<script>const _v = Date.now();</script>
<link rel="stylesheet" id="appCss">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js" integrity="sha384-vsrfeLOOY6KuIYKDlmVH5UiBmgIdB1oEf7p01YgWHuqmOHfZr374+odEv96n9tNC" crossorigin="anonymous"></script>
<script>document.getElementById('appCss').href = 'css/style.css?v=' + _v;</script>
</head>
<body>
Expand Down
86 changes: 58 additions & 28 deletions docs/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -677,7 +684,10 @@
if (pendingMemCharts.length > 0) {
const memHeading = document.createElement("div");
memHeading.className = "scaling-category-heading";
memHeading.innerHTML = `<span>GPU Memory Usage</span><span class="scaling-category-sub">${gpuTotalMemGb ? `Total VRAM: ${formatNumber(gpuTotalMemGb)} GB` : ""}</span>`;
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) {
Expand Down Expand Up @@ -805,7 +815,9 @@
for (const item of items) {
const div = document.createElement("div");
div.className = "info-env-item";
div.innerHTML = `<span class="label">${item.label}</span><span class="value" title="${item.value}">${item.value}</span>`;
const value = textElement("span", "value", item.value);
value.title = String(item.value);
div.append(textElement("span", "label", item.label), value);
container.appendChild(div);
}
}
Expand Down Expand Up @@ -927,9 +939,7 @@
if (warnings.length > 0) {
const banner = document.createElement("div");
banner.className = "warning-banner";
banner.innerHTML = warnings
.map((w) => `<div class="warning-line">\u26A0 ${w}</div>`)
.join("");
banner.append(...warnings);
container.appendChild(banner);
}

Expand Down Expand Up @@ -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);
Expand All @@ -1090,14 +1110,10 @@
const presetPhys = p.physics.toLowerCase();

if (actualPhys && presetPhys && actualPhys !== presetPhys) {
warnings.push(
`Preset <strong>${entry.preset}</strong> configured physics as <strong>${p.physics}</strong>, but <strong>${entry.actual_physics}</strong> was actually used.`
);
addWarning(entry.preset, "physics", p.physics, entry.actual_physics);
}
if (actualRend && p.renderer && !rendererMatches(p.renderer, actualRend)) {
warnings.push(
`Preset <strong>${entry.preset}</strong> configured renderer as <strong>${p.renderer}</strong>, but <strong>${actualRend}</strong> was actually used.`
);
addWarning(entry.preset, "renderer", p.renderer, actualRend);
}
}
return warnings;
Expand Down Expand Up @@ -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
? ` <span class="rank-vram">${formatNumber(vram)} GB / ${pct}% VRAM</span>`
: ` <span class="rank-vram">${formatNumber(vram)} GB VRAM</span>`;
const vramText = pct
? `${formatNumber(vram)} GB / ${pct}% VRAM`
: `${formatNumber(vram)} GB VRAM`;
rankLabel.append(" ", textElement("span", "rank-vram", vramText));
}
li.innerHTML = `<button class="copy-cmd-btn" title="Copy run command">&#x1F4CB;</button><span class="rank-label">${label}${numEnvs ? ` <span class="rank-envs">(${numEnvs} envs)</span>` : ""}${vramStr}</span><span class="rank-value">${formatNumber(item.value)}</span>`;
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() {
Expand Down Expand Up @@ -1347,11 +1369,14 @@
for (const preset of currentOrder) {
const th = document.createElement("th");
const p = parsePreset(preset);
th.innerHTML = `<div class="preset-header-cell">
<span class="preset-part physics">${p.physics}</span>
<span class="preset-part renderer">${p.renderer}</span>
<span class="preset-part datatype">${p.datatype}</span>
</div>`;
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);
Expand Down Expand Up @@ -1436,7 +1461,9 @@
for (const item of items) {
const div = document.createElement("div");
div.className = "version-item";
div.innerHTML = `<span class="version-label">${item.label}</span><span class="version-value" title="${item.value}">${item.value}</span>`;
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);
}
}
Expand Down Expand Up @@ -1805,10 +1832,10 @@
const p = parsePreset(entry.preset);
const div = document.createElement("div");
div.className = "summary-metric";
div.innerHTML = `
<div class="label">${p.physics} / ${p.renderer} / ${p.datatype}</div>
<div class="value">${formatNumber(val)}</div>
`;
div.append(
textElement("div", "label", `${p.physics} / ${p.renderer} / ${p.datatype}`),
textElement("div", "value", formatNumber(val)),
);
content.appendChild(div);
}
}
Expand Down Expand Up @@ -2075,7 +2102,10 @@
const info = allWfTasks[col];
const th = document.createElement("th");
const wfShort = info.workflow.replace("benchmark_", "").replace("_train", "");
th.innerHTML = `<div class="status-col-header"><span>${wfShort}</span><span class="status-task-name">${info.task}</span></div>`;
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);
Expand Down
147 changes: 147 additions & 0 deletions tests/dashboard.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
Loading