From f1a3c06f5f0b1e89bcc50d5b99d1920697055d76 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Mon, 15 Jun 2026 18:20:48 +0200 Subject: [PATCH] pmi: run all class constructors up front for deterministic -cctors output When PMI is invoked with -cctors, initialize every loaded type before any method is JIT'd, instead of lazily/per-type. This covers the target assembly and its whole reference closure, most importantly System.Private.CoreLib. The JIT specializes codegen based on whether a referenced type is already initialized (initClass / getStaticFieldContent): once a type's .cctor has run the JIT can drop class-init helpers, fold 'static readonly' fields and inline more aggressively. Whether a given .cctor happened to run before a particular method was compiled is timing-dependent, which is the main source of non-deterministic PMI diffs under the parallel load of a jit-diff run (e.g. types appearing randomly already-initialized or not between base and diff). Forcing a fully and deterministically initialized class state removes that variability. The existing per-type cctor path is kept as it also initializes the closed generic instantiations PMI creates during the walk. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/pmi/pmi.cs | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/pmi/pmi.cs b/src/pmi/pmi.cs index 2cb9f029..c94c9b90 100644 --- a/src/pmi/pmi.cs +++ b/src/pmi/pmi.cs @@ -938,6 +938,108 @@ public int Work(IEnumerable assemblyNames) return maxResult; } + // When running with "cctors first", initialize every loaded type up front - before any method + // is JIT'd - rather than lazily / per-type. This covers the target assembly and every referenced + // assembly, most importantly System.Private.CoreLib. The JIT specializes codegen based on whether + // a referenced type is already initialized (initClass / getStaticFieldContent): when a type's + // .cctor has run it can drop class-init helpers, fold 'static readonly' fields and inline more + // aggressively. Whether some incidental or background-thread execution happened to run a given + // .cctor before a particular method was compiled is timing-dependent, which is the main source of + // non-deterministic PMI diffs under the parallel load of a jit-diff run. Forcing a fully and + // deterministically initialized class state removes that variability. + private void RunAllClassConstructorsFirst(Assembly target) + { + // Pull in the whole reference closure first so that types referenced only from method bodies + // (and therefore not necessarily loaded yet) are present and get initialized as well. This + // makes the set of initialized types a deterministic function of the assembly's metadata + // rather than of whatever happened to be loaded incidentally. + LoadReferenceClosure(target); + + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + Type[] types; + try + { + types = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + types = e.Types; + } + catch (Exception) + { + continue; + } + + foreach (Type type in types) + { + // Open generic definitions cannot be initialized. + if (type == null || type.ContainsGenericParameters) + { + continue; + } + + try + { + RuntimeHelpers.RunClassConstructor(type.TypeHandle); + } + catch (Exception) + { + // A .cctor may legitimately fail (e.g. platform-specific types); ignore it and keep + // going so the remaining types are still initialized deterministically. + } + } + } + } + + // Force the transitive closure of referenced assemblies to be loaded, using the target's load + // context so that PMIPATH resolution applies. + private static void LoadReferenceClosure(Assembly target) + { + AssemblyLoadContext context = AssemblyLoadContext.GetLoadContext(target) ?? AssemblyLoadContext.Default; + HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase); + Stack pending = new Stack(); + + pending.Push(target); + seen.Add(target.GetName().Name); + + while (pending.Count > 0) + { + Assembly assembly = pending.Pop(); + + AssemblyName[] references; + try + { + references = assembly.GetReferencedAssemblies(); + } + catch (Exception) + { + continue; + } + + foreach (AssemblyName reference in references) + { + if (reference.Name == null || !seen.Add(reference.Name)) + { + continue; + } + + try + { + Assembly loaded = context.LoadFromAssemblyName(reference); + if (loaded != null) + { + pending.Push(loaded); + } + } + catch (Exception) + { + // Some references may be unresolvable in this environment; skip them. + } + } + } + } + public int Work(string assemblyName) { string assemblyPath = Path.GetFullPath(assemblyName); @@ -957,6 +1059,11 @@ public int Work(string assemblyName) visitor.StartAssembly(assembly, assemblyPath); + if (compileAndInvokeCctorsFirst) + { + RunAllClassConstructorsFirst(assembly); + } + bool keepGoing = true; foreach (Type t in types)