dotnet/runtime · error · RuntimeError

Error, unclean replay.

Error message

Error, unclean replay.

What it means

Raised by SuperPMICollection.__verify_final_mch__ when a SuperPMIReplay.replay() over the just-produced final MCH file does not pass cleanly. This is the collection integrity gate: the same JIT used for collection must replay every method context without diagnostics, otherwise the MCH is considered untrustworthy and the pipeline aborts. An unclean replay means the collected data would produce false results in future diff runs.

Solutions

  1. Inspect the SuperPMI log file (under spmi_location) for the specific method context(s) that failed replay and their error codes.
  2. Re-run replay manually: '<superpmi_path> -p <final_mch_file> <jit_path>' to reproduce and capture the failing context.
  3. Confirm the JIT (jit_path) and the collector shim come from the same build/Core_Root.
  4. If the failure is an asserted JIT bug, file or attach to the JIT issue with the failing MCH context; if it is environmental, re-run with more memory/disk.

Example fix

// before
spmi_replay = SuperPMIReplay(self.coreclr_args, mch_files, self.jit_path)
passed = spmi_replay.replay()
if not passed:
    raise RuntimeError("Error, unclean replay.")
// after
spmi_replay = SuperPMIReplay(self.coreclr_args, mch_files, self.jit_path)
passed = spmi_replay.replay()
if not passed:
    raise RuntimeError(f"Unclean replay of {self.final_mch_file} with {self.jit_path}; see log {coreclr_args.log_file} for failing contexts")
Defensive patterns

Strategy: try-catch

Validate before calling

import os
def preflight_replay(final_mch_file, jit_path, superpmi_path):
    for p in (final_mch_file, jit_path, superpmi_path):
        if not os.path.isfile(p):
            raise RuntimeError(f"Replay preflight: missing {p}")
    if os.path.getsize(final_mch_file) == 0:
        raise RuntimeError("MCH file is empty; nothing to replay")

Try / catch

try:
    spmi_replay = SuperPMIReplay(self.coreclr_args, [self.final_mch_file], self.jit_path)
    passed = spmi_replay.replay()
except RuntimeError as e:
    logging.error("Replay infrastructure error: %s", e)
    raise
if not passed:
    raise RuntimeError(f"Unclean replay of {self.final_mch_file}; rerun manually: {self.superpmi_path} -p {self.final_mch_file} {self.jit_path}")

Prevention

When it happens

Trigger: Called during collection finalization when __verify_final_mch__ runs: it builds SuperPMIReplay(self.coreclr_args, [self.final_mch_file], self.jit_path) and calls replay(); if replay() returns False (SuperPMI reported failures or nonzero exit on one or more contexts), the RuntimeError fires. Occurs when the JIT being instrumented has a bug triggered during replay, when the collector shim and replay JIT mismatch, or when the MCH captured incomplete state.

Common situations: Testing an in-development JIT build that hits an assertion or fault during replay; a version skew between the superpmi-shim-collector used to gather and the clrjit used to verify; replaying a cross-arch MCH on the wrong host; transient OS-level failures (e.g. memory pressure) during replay.

Related errors


AI-assisted analysis of dotnet/runtime@60108ba66e (2026-08-10). Data as JSON: /api/errors/7229d7e8f2d2beed. Report an issue: GitHub.

Appendix: source

Thrown at src/coreclr/scripts/superpmi.py:1479

        if not os.path.isfile(self.toc_file):
            raise RuntimeError("Error, toc file not created correctly at: %s" % self.toc_file)

    def __verify_final_mch__(self):
        """ Verify the resulting MCH file is error-free when running SuperPMI against it with the same JIT used for collection.

        Notes:
            <SuperPmiPath> -p -f <s_finalFailMclFile> <s_finalMchFile> <jitPath>
        """

        logging.info("Verifying MCH file")

        mch_files = [ self.final_mch_file ]
        spmi_replay = SuperPMIReplay(self.coreclr_args, mch_files, self.jit_path)
        passed = spmi_replay.replay()

        if not passed:
            raise RuntimeError("Error, unclean replay.")

    def __process_for_ci__(self):
        """ Helix doesn't upload zero-sized files. Sometimes we end up with zero-sized .mch files if
            there is no data collected. Convert these to special "sentinel" files that are later processed
            by "merge-mch" by deleting them. This prevents the <HelixWorkItem.DownloadFilesFromResults>
            file download to succeed (because the file exists) and the MCH merge to also succeed.
        """

        logging.info("Process MCH files for CI")

        if os.path.getsize(self.final_mch_file) == 0:
            # Convert to sentinel file
            logging.info("Converting zero-length MCH file {} to ZEROLENGTH sentinel file".format(self.final_mch_file))
            with open(self.final_mch_file, "w") as write_fh:
                write_fh.write("ZEROLENGTH")

################################################################################
# SuperPMI Replay helpers

View on GitHub (pinned to 60108ba66e)