Skip to content

Share one Kit app across test files instead of booting it per file - #6853

Draft
mataylor-nvidia wants to merge 4 commits into
isaac-sim:developfrom
mataylor-nvidia:mataylor/kit-test-markers
Draft

Share one Kit app across test files instead of booting it per file#6853
mataylor-nvidia wants to merge 4 commits into
isaac-sim:developfrom
mataylor-nvidia:mataylor/kit-test-markers

Conversation

@mataylor-nvidia

Copy link
Copy Markdown

Draft. The last commit adds two temporary CI jobs whose only purpose is to measure whether this is worth continuing. They are meant to be deleted before merge. See Probe below.

Problem

Kit-dependence is currently a property of importing a test file. 156 test modules construct AppLauncher at module scope, so Isaac Sim boots during pytest collection:

simulation_app = AppLauncher(headless=True).app

Nothing declares that dependency, so tools/conftest.py compensates by running every test file in its own subprocess. Kit startup is therefore paid once per file, and there is no way to ask "which tests actually need Kit?" without importing them.

Change

launch_kit() (isaaclab/test/launch.py) replaces the module-scope AppLauncher. It is idempotent: the first module in a process boots Kit, later modules get the running app. It stays at module scope because a test module's own imports (pxr, omni, ...) run at collection, before any fixture could help.

kit / kit_cameras / kitless markers let a file declare its launch configuration, so files that can share a process can be grouped without importing them. kit_solo opts a file out.

test_kit_marker_contract.py keeps the markers from drifting by checking, via AST, that each file's declaration matches what it does at module scope. It is AST-based rather than text-based because several kit-free files mention AppLauncher only in a docstring saying they do not use it — a grep flags those, an AST walk does not.

conftest.py is unchanged.

Scope

source/isaaclab/test/sim is migrated as the pilot: 27 files map to kit, 4 to kit_cameras. The other ~125 files across other packages are untouched and keep working exactly as before — an unmarked file is treated as legacy and still gets its own process. _ENFORCED_ROOTS in the guard is empty, so no file is yet required to carry a marker; it grows per package as migration proceeds.

The transform is applied by tools/codemods/kit_launch_migration.py, added because those ~125 files remain. It edits line ranges in place rather than round-tripping through ast.unparse, which would discard comments and isort directives, and preserves each launch call's position so Kit-dependent imports below it still run after Kit starts.

Ordering constraint

kit and kit_cameras files must not share a process in that order. A camera-enabled app can serve tests that do not need cameras, but cameras cannot be enabled after startup, so launch_kit() raises rather than handing back an app that would silently fail to render.

Relatedly, pytest -m deselects tests but still imports every collected module, so marker filtering alone cannot keep a kit_cameras file out of a kit run. Both probe jobs list their files explicitly for that reason.

Probe (temporary)

kit-reuse-probe-per-file and kit-reuse-probe-batched run the same 30 files from source/isaaclab/test/sim and differ only in how many Kit apps they boot — 30 versus 1. The gap between the two job durations is what reuse is worth per 30 files, and it decides whether migrating the remaining files is justified. Files in TESTS_TO_SKIP are excluded from both sides so the two jobs cover the same tests.

To let a job bypass the per-file orchestrator, run-package-tests gains a test-path input defaulting to "tools" — the previously hard-coded value — so every existing caller is unaffected.

Note that migrating files does not by itself speed up CI, because tools/conftest.py still gives each file its own process. Teaching the runner to batch same-profile files is separate work, deliberately not in this PR.

Incidental fix

The guard found a real bug on its first run: test_operational_space.py assigned pytestmark twice, and the second assignment discarded arm_ci, so that file had been excluded from the ARM CI lane. Merged into a single list.

Testing

  • test_kit_marker_contract.py passes (~1.7s, no Kit). Verified it has teeth by planting five deliberate violations — wrong marker vs. AppLauncher, kitless importing omni, unit calling launch_kit, kit_cameras without cameras=True, two profile markers at once — and confirming each rule fires.
  • uv run isaaclab -f clean.
  • The migrated files themselves have not been run locally; no Isaac Sim on the authoring machine. CI is the first execution, which is part of what this draft is for.

🤖 Generated with Claude Code

@github-actions github-actions Bot added isaac-lab Related to Isaac Lab team infrastructure labels Aug 2, 2026
Kit-dependence is currently a property of importing a test file: 156 test
modules construct AppLauncher at module scope, so Isaac Sim boots during
pytest collection. Because nothing declares that dependency, tools/conftest.py
has to run every test file in its own subprocess, paying Kit startup once per
file.

Introduce the two pieces needed to change that:

launch_kit() is an idempotent module-scope replacement for AppLauncher. The
first test module in a process boots Kit; later modules receive the running
app, so a pytest run covering several files pays startup once. It raises
rather than silently returning a mismatched app when a file asks for cameras
after a camera-less boot.

The kit / kit_cameras / kitless markers let a file declare which launch
configuration it needs, so files that can share a process can be grouped
without importing them. kit_solo opts a file out of any such grouping.

test_kit_marker_contract.py keeps the markers from drifting: it checks by AST
that a file's declaration matches what it does at module scope. The checks are
AST-based rather than text-based because several kit-free files mention
AppLauncher only in a docstring saying they do not use it. Files are not yet
required to carry a marker; _ENFORCED_ROOTS is empty and grows per package as
files are migrated.

No test file changes behaviour: nothing is marked kit or kitless yet, and no
file calls launch_kit() yet.

The guard found one pre-existing bug on its first run. test_operational_space
assigned pytestmark twice, and the second assignment discarded arm_ci, so the
file had been excluded from the ARM CI lane. Merged into a single list.
@mataylor-nvidia
mataylor-nvidia force-pushed the mataylor/kit-test-markers branch from 8c337c5 to 57ffdbd Compare August 2, 2026 20:33
Replace the module-scope AppLauncher construction in the Kit-dependent files
under source/isaaclab/test/sim with launch_kit(), and declare the matching
kit or kit_cameras marker on each file.

Because launch_kit() is idempotent, a pytest process covering several of
these files now boots Kit once instead of once per file. Nothing forces them
into one process yet -- tools/conftest.py still runs a subprocess per file --
so this changes how the files launch Kit, not how CI schedules them.

24 files map to `kit` and 4 to `kit_cameras`. The two groups must not share a
process in that order: a camera-enabled app can serve tests that do not need
cameras, but cameras cannot be enabled after startup, so launch_kit() raises
rather than handing back an app that would silently fail to render.

The transform is applied by tools/codemods/kit_launch_migration.py, added
here because ~125 files in other packages remain to migrate. It edits line
ranges in place rather than round-tripping through ast.unparse, which would
discard comments and isort directives, and it preserves each launch call's
position so the Kit-dependent imports below it still run after Kit starts.

The codemod refuses anything it cannot rewrite without changing behaviour,
and reports it. In particular it rejects a conditional launch such as
`AppLauncher(...).app if _USE_KIT else None`, which test_mjcf_converter.py
and test_urdf_converter.py use so they can run kitlessly when the standalone
importer wheel is installed; collapsing that ternary would have made the boot
unconditional. It also refuses a file that references AppLauncher for
anything other than the launch call, since the import is removed.
Whether migrating the remaining ~125 test files off module-scope AppLauncher
is worth doing depends on how much Kit startup actually costs, which is not
something the current pipeline reports directly.

Add two temporary jobs that run the same 30 files from
source/isaaclab/test/sim and differ only in how many Kit apps they boot.
kit-reuse-probe-per-file keeps the default test-path of "tools", so
tools/conftest.py gives each file its own subprocess and Kit boots 30 times.
kit-reuse-probe-batched points pytest at the files directly, so they share
one process and launch_kit() boots Kit once. The difference between the two
job durations is what reuse is worth per 30 files.

Both jobs list their files explicitly instead of selecting with `-m kit`,
because pytest's marker filtering deselects tests but still imports every
collected module, and importing a kit_cameras module calls
launch_kit(cameras=True) regardless of whether its tests will run. The
batched job lists the four kit_cameras files first: a camera-enabled app can
serve tests that do not need cameras, but cameras cannot be enabled after
startup, so the opposite order makes launch_kit() raise. Files in
TESTS_TO_SKIP are excluded from both sides so the jobs cover the same tests.

To let a job bypass the per-file orchestrator, run-package-tests gains a
test-path input. It defaults to "tools", the value that was previously
hard-coded, so every existing caller is unaffected.

Both jobs are continue-on-error and are meant to be deleted once the
measurement is recorded.
@mataylor-nvidia
mataylor-nvidia force-pushed the mataylor/kit-test-markers branch from 57ffdbd to ded9dfd Compare August 2, 2026 22:19
The per-file runner grants the first camera-enabled test file an extra 700 s
of timeout, because that file compiles RTX shaders (~600 s) on a cold cache.
It identified such files by searching their source for the literal string
"enable_cameras=True".

Migrating a file to launch_kit(cameras=True) removes that literal, so the
buffer stopped being applied and the file was killed at the 120 s startup
deadline instead. That is what happened to
test_simulation_stage_in_memory.py in the kit-reuse-probe-per-file job: it
was reported as a startup hang at 120.94 s having run no tests.

Match the marker and the launch_kit call as well as the old literal, so the
buffer applies both before and after a file is migrated.

Also narrow the probe to the 24 `kit` files and drop the four `kit_cameras`
ones from both sides. The cold shader compile is roughly thirty times the Kit
startup the probe is trying to measure, so including those files tells us
about shader caching rather than about app reuse.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

infrastructure isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant