(release/0.3.0) Release viewport capture before stage teardown when Replicator is included - #1215
(release/0.3.0) Release viewport capture before stage teardown when Replicator is included#1215xyao-nv wants to merge 2 commits into
Conversation
Signed-off-by: Xinjie Yao <xyao@nvidia.com>
The PR should not merge until viewport-recorder finalization is incorporated into the guaranteed cleanup path so an encoder or output failure cannot bypass simulator teardown. Findings
|
| if annotator is not None: | ||
| with suppress(Exception): | ||
| annotator.detach() | ||
| capture._rgb_annotator = None | ||
| if render_product is not None: | ||
| with suppress(Exception): | ||
| render_product.destroy() | ||
| capture._render_product = None |
There was a problem hiding this comment.
🟡 This suppresses the exact failure the PR is fixing
If detach() or destroy() raises, we swallow it, null the attribute anyway, and the shutdown hang comes back with nothing in the log. Could we log it instead of dropping it silently? (Needs import logging + logger = logging.getLogger(__name__) at module level.)
| if annotator is not None: | |
| with suppress(Exception): | |
| annotator.detach() | |
| capture._rgb_annotator = None | |
| if render_product is not None: | |
| with suppress(Exception): | |
| render_product.destroy() | |
| capture._render_product = None | |
| if annotator is not None: | |
| try: | |
| annotator.detach() | |
| except Exception: | |
| logger.warning("Could not detach the viewport RGB annotator; stage teardown may hang.", exc_info=True) | |
| capture._rgb_annotator = None | |
| if render_product is not None: | |
| try: | |
| render_product.destroy() | |
| except Exception: | |
| logger.warning("Could not destroy the viewport render product; stage teardown may hang.", exc_info=True) | |
| capture._render_product = None |
| return "rgb_array" if self.record_viewport_video else None | ||
|
|
||
|
|
||
| def close_viewport_video_recorder(video_recorder) -> None: |
There was a problem hiding this comment.
🟡 Goes quiet if Isaac Lab renames these privates
Every step here is a getattr on an Isaac Lab private — _capture, _rgb_annotator, _render_product, _viewer. If any of those get renamed upstream, this function silently does nothing, the hang returns, and the new unit tests still pass because they build their own SimpleNamespace with the same names. Could we log a warning when none of the known release paths matched, so a broken shim shows up in the run log rather than as a mystery timeout?
Worth a NOTE here too, pointing at the upstream gap (neither VideoRecorder nor IsaacsimKitPerspectiveVideo/NewtonGlPerspectiveVideo has a close() in 3.0), so this whole function can be deleted once that lands.
Small thing: video_recorder has no annotation — VideoRecorder | None under TYPE_CHECKING would make it obvious whose internals we're reaching into.
| """ | ||
| return self.metrics_manager.compute() | ||
|
|
||
| def close(self) -> None: |
There was a problem hiding this comment.
🟡 The same cleanup is now wired up in two places
close_environment() releases the capture before teardown_simulation_app(), then calls env.close() — so by the time this override runs, _capture is already None and the stage is already replaced. It's a no-op on the evaluation path; it only does real work for the direct env.close() callers in isaaclab_arena_examples.
Would it be simpler to keep the release here only, and have close_environment() call env.close() before teardown_simulation_app()? Then one place owns it. If the current teardown-then-close order is load-bearing, a short NOTE in close_environment saying why would help — I couldn't find a reason for it in the history.
| assert recorder._capture is None | ||
|
|
||
|
|
||
| def test_close_viewport_video_recorder_uses_public_close(): |
There was a problem hiding this comment.
🔵 This branch isn't reachable against real Isaac Lab objects
Neither VideoRecorder nor the Kit/Newton capture classes define close() in 3.0, so both close() fast-paths are forward-compat only. Fine to keep, but a one-line comment saying so would stop it reading as a path that's actually exercised.
Same idea for the other test: the fake mirrors IsaacsimKitPerspectiveVideo's private field names, so naming that class in a comment tells the next person where to re-check after an Isaac Lab bump.
🤖 Isaac Lab-Arena Review BotSummaryReleases the Kit/Newton viewport capture (Replicator annotator + render product) before the USD stage is replaced, so an evaluation with Design, Boundaries & ScopeThe cleanup is wired up both in Separately, Findings
Test CoverageTwo new plain unit tests, no sim — they land correctly in Phase 1 and don't need the inner/outer pattern or markers, matching the existing Copyright years check out: Minor: the PR description still has the VerdictMinor fixes needed |
Co-authored-by: arena-review-bot[bot] <290456231+arena-review-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
The ideal fix looks like would belong to IsaacLab where ManagerBasedEnv owns the VideoRecorder. So VideoRecorder.close() should release its capture, and ManagerBasedEnv.close() should call it... Shall we add a TODO() to eventually clean this up properly in Lab?
| def close(self) -> None: | ||
| """Release viewport capture before closing the simulation environment.""" | ||
| try: | ||
| close_viewport_video_recorder(getattr(self, "video_recorder", None)) |
There was a problem hiding this comment.
We should have access to self.recorder here directly:
close_viewport_video_recorder(self.video_recorder)
If Isaac Lab removes or renames the attribute, the code fails visibly instead of silently skipping the cleanup
| return "rgb_array" if self.record_viewport_video else None | ||
|
|
||
|
|
||
| def close_viewport_video_recorder(video_recorder) -> None: |
There was a problem hiding this comment.
Second on the use of getattr()... we could use the known fields directly:
def close_viewport_video_recorder(
video_recorder: VideoRecorder | None,
) -> None:
"""Release the Kit viewport capture used by the pinned Isaac Lab version."""
if video_recorder is None or video_recorder._backend != "kit":
return
capture = video_recorder._capture
if capture is None:
return
annotator = capture._rgb_annotator
render_product = capture._render_product
| @@ -30,9 +31,11 @@ def close_policy(policy: PolicyBase | None) -> None: | |||
|
|
|||
|
|
|||
| def close_environment(env: gym.Env | None) -> None: | |||
There was a problem hiding this comment.
Is this required explicetly to call close_viewport_video_recorder() here seperately?
Suggestion to keep cleanup only in IsaacLabArenaManagerBasedRLEnv.close() , then the below call of env.close() in
finally:
try:
env.close()
finally:
collect_garbage_and_clear_cuda_cache()
would triggered to close it
cvolkcvolk
left a comment
There was a problem hiding this comment.
Thanks for addressing this!
I wasn't able to reproduce the shutdown hang locally, nor find Replicator to be the root cause. Left a few comments. Ok from my side to unblock for now but I think the root cause should be fixed in IsaacLab
Summary
Short description of the change (max 50 chars)
Detailed description
Fixes:
Before
MP4 is written, process remains around “Closing simulation app,” and timeout eventually returns 124; index.html is absent.
After
Process exits 0, and both the MP4 and index.html are present.