Skip to content
Closed
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
18 changes: 18 additions & 0 deletions docs/_extensions/isaaclab_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import annotations

import csv
import inspect
import json
import posixpath
Expand Down Expand Up @@ -84,6 +85,22 @@ def _parse_rst(directive: SphinxDirective, content: str) -> list[nodes.Node]:
return container.children


class IsaacLabBenchmarkData(SphinxDirective):
"""Embed benchmark snapshots so downloaded HTML works without an HTTP server."""

def run(self) -> list[nodes.Node]:
snapshots = {}
for channel in ("release", "develop"):
path = Path(self.env.srcdir) / "source/_static/benchmarks" / f"environment-performance-{channel}.csv"
self.env.note_dependency(str(path))
with path.open(encoding="utf-8", newline="") as stream:
snapshots[channel] = list(csv.DictReader(stream))
# Prevent CSV text from closing the script element, even inside a JSON string.
payload = json.dumps(snapshots).replace("<", "\\u003c")
html = f'<script type="application/json" data-environment-benchmark-rows>{payload}</script>'
return [nodes.raw("", html, format="html")]


class IsaacLabCloneCommands(SphinxDirective):
"""Render SSH/HTTPS clone tabs using copy-friendly ``code-block`` directives."""

Expand Down Expand Up @@ -283,6 +300,7 @@ def setup(app):
app.add_config_value("torchvision_version", "", "env")
app.add_config_value("ovrtx_spec", "", "env")
app.add_role("isaaclab-source", IsaacLabSourceLink())
app.add_directive("isaaclab-benchmark-data", IsaacLabBenchmarkData)
app.add_directive("isaaclab-clone-commands", IsaacLabCloneCommands)
app.add_directive("isaaclab-clone-https", IsaacLabCloneHttps)
app.add_directive("isaaclab-uv-isaacsim-wheel-install", IsaacLabUvIsaacSimWheelInstall)
Expand Down
40 changes: 5 additions & 35 deletions docs/source/_static/css/environment-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -642,32 +642,6 @@
updateSelection();
};

const parseCsv = (contents) => {
const parseLine = (line) => {
const values = [];
let value = "";
let quoted = false;
for (let index = 0; index < line.length; index += 1) {
const character = line[index];
if (character === '"' && quoted && line[index + 1] === '"') {
value += '"';
index += 1;
} else if (character === '"') {
quoted = !quoted;
} else if (character === "," && !quoted) {
values.push(value);
value = "";
} else {
value += character;
}
}
values.push(value);
return values;
};
const [header, ...lines] = contents.trim().split(/\r?\n/).map(parseLine);
return lines.map((line) => Object.fromEntries(header.map((name, index) => [name, line[index]])));
};

const formatFps = (value) => {
if (value >= 1_000_000) {
return `${Number((value / 1_000_000).toFixed(2))}M`;
Expand Down Expand Up @@ -931,18 +905,14 @@
chart.replaceChildren(...(rows.length ? [renderBenchmarkChart(rows, maximum)] : []));
};

const renderBenchmarks = async () => {
const renderBenchmarks = () => {
if (!benchmarks) {
return;
}
benchmarkRows = (await Promise.all(["release", "develop"].map(async (channel) => {
const snapshots = JSON.parse(document.querySelector("[data-environment-benchmark-rows]").textContent);
benchmarkRows = ["release", "develop"].flatMap((channel) => {
try {
const source = new URL(benchmarks.getAttribute(`data-benchmark-${channel}-source`), window.location.href);
const response = await fetch(source);
if (!response.ok) {
throw new Error(`Benchmark request failed with ${response.status}`);
}
return parseCsv(await response.text())
return snapshots[channel]
.filter((row) => row.data_origin === "measured" && row.workload === "training")
.filter((row) => [row.collection_fps_mean, row.total_fps_mean]
.every((value) => Number.isFinite(Number(value)) && Number(value) > 0))
Expand All @@ -955,7 +925,7 @@
console.error(error);
return [];
}
}))).flat();
});
updatePreview();
};

Expand Down
4 changes: 2 additions & 2 deletions docs/source/setup/environments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ Task Preview
Benchmarks
----------

.. isaaclab-benchmark-data::

.. raw:: html

<div class="environment-browser" data-environment-benchmarks
data-benchmark-release-source="../../_static/benchmarks/environment-performance-release.csv"
data-benchmark-develop-source="../../_static/benchmarks/environment-performance-develop.csv"
data-benchmark-release-dates="2026-09-09"
data-benchmark-develop-dates="2026-08-28,2026-09-09">
<section class="environment-benchmark-panel">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Fixed simulation startup failing with a missing ``sys`` import in the platform-specific device check,
including demos launched through ``uvx``.
1 change: 1 addition & 0 deletions source/isaaclab/isaaclab/app/sim_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import argparse
import logging
import os
import sys
import traceback
from collections.abc import Generator
from contextlib import contextmanager
Expand Down
57 changes: 57 additions & 0 deletions tools/test/test_doc_benchmarks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Tests for self-contained benchmark snapshots in built documentation."""

import csv
import json
import re
import subprocess
import sys
from pathlib import Path


def test_benchmark_snapshots_are_embedded_and_refresh_on_csv_changes(tmp_path):
"""HTML preserves both CSV snapshots safely and tracks changes during incremental builds."""
extensions = Path(__file__).resolve().parents[2] / "docs/_extensions"
source = tmp_path / "docs"
snapshots = source / "source/_static/benchmarks"
snapshots.mkdir(parents=True)
(source / "conf.py").write_text(
f"import sys\nsys.path.insert(0, {str(extensions)!r})\nextensions = ['isaaclab_docs']\n",
encoding="utf-8",
)
(source / "index.rst").write_text("Benchmarks\n==========\n\n.. isaaclab-benchmark-data::\n", encoding="utf-8")
rows = {
"release": [{"task": "Isaac-Cartpole", "collection_fps_mean": "100", "notes": '</script>\nquoted "value",'}],
"develop": [{"task": "Isaac-Cartpole", "collection_fps_mean": "200", "notes": "historical snapshot"}],
}

def write_snapshot(channel):
with (snapshots / f"environment-performance-{channel}.csv").open("w", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=["task", "collection_fps_mean", "notes"])
writer.writeheader()
writer.writerows(rows[channel])

def build_snapshots():
output = tmp_path / "html"
subprocess.run(
[sys.executable, "-m", "sphinx", "-W", "-q", str(source), str(output)],
check=True,
capture_output=True,
text=True,
)
html = (output / "index.html").read_text(encoding="utf-8")
payload = re.search(r'<script type="application/json" data-environment-benchmark-rows>(.*?)</script>', html)
assert payload is not None
return json.loads(payload.group(1))

for channel in rows:
write_snapshot(channel)
assert build_snapshots() == rows

rows["develop"][0]["collection_fps_mean"] = "300"
write_snapshot("develop")
assert build_snapshots() == rows
Loading