Skip to content

Record Arena git provenance and each Run's end time in the experiment result - #1540

Open
david-tingdahl-nvidia wants to merge 4 commits into
mainfrom
dtingdahl/feature/experiment-provenance-end-time
Open

david-tingdahl-nvidia wants to merge 4 commits into
mainfrom
dtingdahl/feature/experiment-provenance-end-time

Conversation

@david-tingdahl-nvidia

@david-tingdahl-nvidia david-tingdahl-nvidia commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Record the Arena git commit and each Run's end time in the experiment result.

Why this change?

We are building a KPI reporting framework. To track results over time, each result must say which Arena commit and
branch produced it and when each Run ended. The experiment result had neither. The commit and branch are now
available inside the image.

What changes?

  • New GitStamp dataclass reads the Arena commit and branch from the environment.
  • The experiment result file now includes this stamp and an end_timestamp_ns for every Run.

@david-tingdahl-nvidia

Copy link
Copy Markdown
Collaborator Author

/review

@greptile-apps

greptile-apps Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The change reaches saved results across direct and OSMO experiment runs.

The PR appears safe to merge, with a non-blocking concern about preserving the execution commit when collecting saved results.

Findings

  1. P2 Results can name wrong commits ▶

Summary

Adds the Arena git commit and branch to experiment results and records each run's end time.

  • Experiment results record Arena provenance and each Run's end time.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Local run finishes or fails] --> B[Record time.time_ns]
  B --> C[Write experiment result]
  D[OSMO runner process exits] --> E[Record shell end time]
  E --> F[Write runner result]
  F --> G[Collect run outputs]
  G --> C
  H[Writer environment] --> I[Read git commit and branch]
  I --> C
Loading

Reviews (1) · Last reviewed commit: "evaluation: record Arena git provenance ..." · Reviewed by Greptile

"""
assert run_metadata_by_name, "run_metadata_by_name must not be empty"
self.experiment_output_directory = Path(experiment_output_directory)
self._provenance = provenance if provenance is not None else Provenance.from_environment()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Results can name wrong commits

ArenaExperimentResult reads the commit and branch from the machine collecting the results. build_experiment_output uses this default, while the OSMO runner result does not carry provenance. If saved outputs are collected in a newer image, old runs get labeled with the newer commit, making comparisons between versions unreliable. Preserve the runner's provenance in its output and pass it explicitly when collecting results.

Comment thread osmo/tasks/experiment_runner_task.py Outdated
)
write_experiment_runner_result_command = (
'printf \'{"execution_status":"%s","process_exit_code":%d,"runs":%s}\\n\' '
'printf \'{"execution_status":"%s","process_exit_code":%d,"end_timestamp_ns":%d,"runs":%s}\\n\' '

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Collector records its own commit, not the Runs'

build_experiment_output creates ArenaExperimentResult without a provenance, so the commit comes from the collector container. That container starts later, from the same :latest tag that ci.yml moves on every push to main. A merge during a long Experiment would label the results with a newer commit than the one that produced them. Separate Run groups could even run different commits.

Could this printf also write $ARENA_GIT_SHA and $ARENA_GIT_BRANCH? Then build_experiment_output could check that every Run reports the same values and pass a Provenance into ArenaExperimentResult. That would give the new provenance parameter its first production caller; today only a test passes it.

Comment thread isaaclab_arena/evaluation/provenance.py Outdated
), f"provenance {provenance_field.name} must be a non-empty single-line string, got {value!r}"

@classmethod
def from_environment(cls) -> Provenance:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Stale commit in the dev container

run_docker.sh mounts the host checkout over the image's copy and only rebuilds the image with -r. So ARENA_GIT_SHA keeps the commit and -dirty state from the last image build while the mounted code moves on. Results from local runs, and the KPI records that #1541 writes from them, would then name a commit that didn't produce them.

Could run_docker.sh pass --env ARENA_GIT_SHA= --env ARENA_GIT_BRANCH=? This method already maps empty values to unknown, so local runs would record unknown. Or should this read the commit from the checkout with git when .git is present?

Comment thread isaaclab_arena/evaluation/arena_run.py Outdated
metrics: MetricsDataCollection | None = None
"""Metrics aggregated over all successful rebuilds, when provided by the task."""

end_timestamp_ns: int | None = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 end_timestamp_ns could be required

The None state only exists between build_and_run returning and the replace(...) in execute_experiment. Because of it, _write_arena_experiment_result needs the nested _end_timestamp_ns assert.

If build_and_run stamped its own result with end_timestamp_ns=time.time_ns(), this could be a required end_timestamp_ns: int declared before metrics. The replace and the nested helper could then go. The fake build_and_run functions in test_run_execution.py would set the value themselves, and a build_and_run test would need to assert it for the success path.



@pytest.mark.parametrize("status", ["completed", "failed"])
def test_run_metadata_end_timestamp_is_written(tmp_path, status):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Duplicate test

test_collects_declared_runs_rebuilds_and_rank_files_without_changing_episode_objects already checks end_timestamp_ns for both a completed and a failed Run through its full to_dict() comparison. This test doesn't add coverage. Could we drop it? It also repeats the literal instead of using END_TIMESTAMP_NS.

}],
}]
assert set(first_run_result) == {"environment", "policy_variant", "status", "rebuilds"}
assert first_run_result["end_timestamp_ns"] == END_TIMESTAMP_NS

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Both Runs share one end time

Both runner results use the default END_TIMESTAMP_NS, so this test passes even if the collector gave every Run the same time.

Could the second runner pass end_timestamp_ns=END_TIMESTAMP_NS + 1, and this test also check experiment_result["runs"]["second"]["end_timestamp_ns"]? That would also give the new end_timestamp_ns parameter of _write_experiment_runner_result its first caller; no call passes it today.

@arena-review-bot

arena-review-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Isaac Lab-Arena Review Bot

Summary

This PR records the Arena git commit and branch (Provenance) and an end_timestamp_ns for each Run in arena_experiment_result.json. It covers both the local Experiment Runner path and the OSMO collector path. The earlier inline threads on this commit still apply and aren't repeated here; this pass adds three small findings. The main open question is still where the commit is read: the process that writes the result isn't always running the code that ran the Runs.

Design, Boundaries & Scope

  • Where provenance is read. Neither production caller passes provenance to ArenaExperimentResult, so it falls back to Provenance.from_environment() in the process that writes the file.
    • In OSMO, that's the collector container.
    • In the dev container, ARENA_GIT_SHA can be older than the mounted code.
    • See the open threads on osmo/tasks/experiment_runner_task.py:135 and isaaclab_arena/evaluation/provenance.py:42.
  • Do we need an end time per Run? Write KPIs as InfluxDB-ready records #1541 is the only reader, and it takes the max() over Runs.
    • The two producers also measure different moments. Locally it's when build_and_run returns. In OSMO it's after the whole runner process exits.
    • If one Experiment-level time is enough, these could all go: the ArenaRunResult field, the date +%s%N step and the new field in the runner result.
  • Rollout. The collector runs build_experiment_output.py from the submitter's checkout but imports ArenaExperimentResult from the image. Once :latest includes this PR, a workflow submitted from an older checkout won't pass end_timestamp_ns, and _collect_run fails with a KeyError.

Findings

New in this pass:

🔵 Improvement: osmo/scripts/build_experiment_output.py:57: end_timestamp_ns is checked here and again in ArenaExperimentResult._collect_run, so this could just pass it through. The Returns: docstring is also out of date.
🔵 Improvement: isaaclab_arena/evaluation/provenance.py:34: the single-line check is an InfluxDB tag rule that #1541's records already enforce, so Provenance.__post_init__ could go.
🔵 Improvement: isaaclab_arena/tests/test_osmo_experiment_workflow.py:255: no test loads the file the runner script writes with load_experiment_runner_result.

Still open from the earlier inline threads:

🟡 Warning: osmo/tasks/experiment_runner_task.py:135: the collector records its own commit, not the commit that ran the Runs.
🟡 Warning: isaaclab_arena/evaluation/provenance.py:42: ARENA_GIT_SHA can be out of date in the dev container.
🔵 Improvement: isaaclab_arena/evaluation/arena_run.py:123: end_timestamp_ns could be a required int.
🔵 Improvement: isaaclab_arena/tests/test_arena_experiment_result.py:160: this test repeats what the main collection test already checks.
🔵 Improvement: isaaclab_arena/tests/test_osmo_build_experiment_output.py:310: both Runs share one end time.

Test Coverage and CI Cost

  • The shell script that writes experiment_runner_result.json and the Python loader are each tested against their own hand-written copy of the format, never against each other. One short bash run in the existing with_subprocess file would close that gap (inline on test_osmo_experiment_workflow.py:255).
  • CI hasn't run the tests on this commit yet. The earlier CI run skipped the test jobs, and the current run was still building the image when I checked. The PR also says the Isaac Sim tests in test_experiment_runner.py weren't run locally. So the updated end_timestamp_ns assertion in test_experiment_runner_from_typed_yaml hasn't run anywhere yet.

Verdict

Minor fixes needed

Comment thread osmo/scripts/build_experiment_output.py Outdated
}, f"Experiment Runner result for Run '{run_name}' must contain metadata for exactly that Run"
run_metadata = run_metadata_by_name[run_name]
assert isinstance(run_metadata, dict), f"Experiment Runner metadata for Run '{run_name}' must be a JSON object"
end_timestamp_ns = experiment_runner_result["end_timestamp_ns"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 end_timestamp_ns is checked twice

This value only goes into ArenaExperimentResult, and _collect_run (arena_experiment_result.py:144) runs the same int check on it. Could this just pass it through, and lines 57–60 go?

        {**run_metadata, "end_timestamp_ns": experiment_runner_result["end_timestamp_ns"]},

The Returns: line in the docstring also still says "environment and policy metadata". It now includes the end time too.

Comment thread isaaclab_arena/evaluation/git_stamp.py Outdated
git_branch: str
"""Git branch name, or ``"unknown"``."""

def __post_init__(self) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Is __post_init__ needed?

Git SHAs and branch names can't contain line breaks, and from_environment never returns an empty value. The single-line rule comes from InfluxDB tags, and #1541's InfluxdbKpiRecord.__post_init__ already checks every tag, git_sha and git_branch included.

Could we drop __post_init__ and keep the exact-key check in from_dict? The empty, line-break and None cases in test_provenance_rejects_invalid_values would go with it.

assert '"runs":%s' in experiment_runner_command
assert '"end_timestamp_ns":%d' in experiment_runner_command
# The end time is taken once the Experiment Runner process has exited, before the result is written.
assert "fi\nexperiment_runner_end_timestamp_ns=$(date +%s%N)\n" in experiment_runner_command

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Script output is never loaded

These asserts check pieces of the printf line, not the file it writes. If two arguments get swapped, they all still pass. For a failed Run, swapping the exit code and the end time would even load cleanly. test_osmo_build_experiment_output.py writes its runner results with a Python helper, so nothing checks the shell output against the loader.

Could one test run the rendered script with a stub isaaclab.sh and load the result? This file is already with_subprocess, so it adds one short bash process. It could also replace the asserts on lines 253, 255 and 256. A sketch (not run):

def test_experiment_runner_script_writes_a_result_the_collector_loads(tmp_path):
    task = ExperimentRunnerTask(
        task_cfg=ExperimentRunnerTaskCfg(),
        experiment_cfg=_zero_action_experiment_cfg(),
        lead=True,
        task_name="experiment-runner",
    )
    stub_bin_directory = tmp_path / "bin"
    stub_bin_directory.mkdir()
    stub_isaaclab_script = stub_bin_directory / "isaaclab.sh"
    stub_isaaclab_script.write_text("#!/bin/bash\nexit 3\n")
    stub_isaaclab_script.chmod(0o755)
    run_script = task._get_run_script().replace(OSMO_TASK_OUTPUT_DIR, str(tmp_path))

    before_ns = time.time_ns()
    subprocess.run(
        ["bash", "-c", run_script],
        env={**os.environ, "PATH": f"{stub_bin_directory}:{os.environ['PATH']}"},
        check=True,
    )
    after_ns = time.time_ns()

    run_execution_report, run_metadata = load_experiment_runner_result(tmp_path, "baseline")
    assert run_execution_report.status is RunStatus.FAILED
    assert run_execution_report.process_exit_code == 3
    assert before_ns <= run_metadata["end_timestamp_ns"] <= after_ns

@david-tingdahl-nvidia
david-tingdahl-nvidia force-pushed the dtingdahl/feature/experiment-provenance-end-time branch from 9d093c4 to 199e51a Compare October 29, 2026 17:07
@david-tingdahl-nvidia
david-tingdahl-nvidia force-pushed the dtingdahl/feature/experiment-provenance-end-time branch from 199e51a to 5aeeddd Compare October 30, 2026 08:07
class ArenaExperimentResultData(TypedDict):
"""Contents of an Arena Experiment result file."""

git_stamp: dict[str, str]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be : GitStamp ?

Comment thread isaaclab_arena/evaluation/git_stamp.py Outdated
"""Value recorded when the git commit or branch is not known."""

ARENA_GIT_SHA_ENV_VAR = "ARENA_GIT_SHA"
"""Environment variable holding the Arena git commit SHA; the Arena Docker image sets it."""

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should not discuss the docker image here. it's a layer above us

Comment thread isaaclab_arena/evaluation/git_stamp.py Outdated

@classmethod
def from_environment(cls) -> GitStamp:
"""Return the git stamp from ``ARENA_GIT_SHA`` and ``ARENA_GIT_BRANCH``, ``"unknown"`` when unset or empty."""

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enough to say "from environment"

Base automatically changed from dtingdahl/chore/docker-git-stamp to main October 30, 2026 10:37
@david-tingdahl-nvidia
david-tingdahl-nvidia force-pushed the dtingdahl/feature/experiment-provenance-end-time branch from 5aeeddd to ab19c3a Compare October 30, 2026 10:37

@alexmillane alexmillane left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks!

environment: dict[str, str]
policy_variant: str
status: Literal["completed", "failed"]
end_timestamp_ns: int

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we add start_timestamp_ns for symmetry?


ArenaExperimentResultData = dict[Literal["runs"], dict[str, ArenaExperimentRunData]]
class ArenaExperimentResultData(TypedDict):
"""Contents of an Arena Experiment result file."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion to lengthen to say what the maps contain. I guess they map from run name to run data?

process_exit_code=process_exit_code,
),
run_metadata,
{**run_metadata, "end_timestamp_ns": experiment_runner_result["end_timestamp_ns"]},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is a bit hard to read. Consider adding the timestamp to the metadata outside of this big return statement.

Add a GitStamp (git commit and branch of the Arena source, read from ARENA_GIT_SHA and ARENA_GIT_BRANCH) and each Run's end time to arena_experiment_result.json, and forward both to the OSMO experiment output.

Signed-off-by: David Tingdahl <dtingdahl@nvidia.com>
…mments

Signed-off-by: David Tingdahl <dtingdahl@nvidia.com>
Signed-off-by: David Tingdahl <dtingdahl@nvidia.com>
Signed-off-by: David Tingdahl <dtingdahl@nvidia.com>
@david-tingdahl-nvidia
david-tingdahl-nvidia force-pushed the dtingdahl/feature/experiment-provenance-end-time branch from 682d1a7 to 9849580 Compare October 30, 2026 17:10

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants