From ebdfeb349f72cdd0c7d00575ef38380df602275e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= <6334612+rbuergi@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:07:14 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(#890):=20run=20every=20compiling=20host?= =?UTF-8?q?=20with=20Tiered=20PGO=20off=20=E2=80=94=20the=20.NET=2010=20JI?= =?UTF-8?q?T=20drops=20the=20isinst=20in=20Roslyn's=20inlined=20Containing?= =?UTF-8?q?Type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Memex.Portal.Shared.Test part 1/2 poisoned in 4 of 12 main runs since the class split moved InstallNeverInheritsRepoCompileVerdictTest behind the suite's stand-in emits: every Emit in the host then throws NRE in NamedTypeSymbol.get_ContainingTypeDefinition (#890), failing InstallNeverInherits…, both ModulesUpdateIndependentlyOfThePlatformTest cases, or all of ModuleLinkVersionTest — on unrelated PRs. Reproduced in CI's own shape (run 37647154163, the poisoned PR's build-output-3, part 1 launched as the shard launches it, 14 runners per arm): defaults 4 poisoned / 10 clean, DOTNET_TieredPGO=0 0 / 14. The Tier-1 listing of NamedTypeSymbol.AsNestedTypeDefinitionImpl explains it 14 of 14: the inlined SourceMemberContainerTypeSymbol.ContainingType (`_containingSymbol as NamedTypeSymbol`) keeps its isinst in all 10 clean hosts and has none in all 4 poisoned ones, so a top-level type's namespace container reads as its containing type. TieredPGO=false in the root and test Directory.Build.props reaches every host's runtimeconfig (System.Runtime.TieredPGO). TieredPgoIsOffInEveryCompilingHostTest pins it; negative control: a build with -p:TieredPGO=true fails both assertions. Workaround for a runtime defect, documented as one in NodeTypeCompilation.md ("The defect, in one listing"). Co-Authored-By: Claude Opus 5.5 (1M context) --- Directory.Build.props | 12 ++++ .../Data/Architecture/NodeTypeCompilation.md | 55 ++++++++++++++++++ test/Directory.Build.props | 6 ++ .../TieredPgoIsOffInEveryCompilingHostTest.cs | 56 +++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 test/Memex.Portal.Shared.Test/TieredPgoIsOffInEveryCompilingHostTest.cs diff --git a/Directory.Build.props b/Directory.Build.props index 4c4c39fa67..49bae9d8bd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -28,6 +28,18 @@ toward leaving a dead package listed. Opting a project back in means intending to publish it, and means adding it to .github/workflows/publish-packages.yml. --> false + + false false diff --git a/src/MeshWeaver.Documentation/Data/Architecture/NodeTypeCompilation.md b/src/MeshWeaver.Documentation/Data/Architecture/NodeTypeCompilation.md index c0f0d5ecdb..26fe650a22 100644 --- a/src/MeshWeaver.Documentation/Data/Architecture/NodeTypeCompilation.md +++ b/src/MeshWeaver.Documentation/Data/Architecture/NodeTypeCompilation.md @@ -2144,6 +2144,61 @@ per-process rate above is a test-host rate and says nothing about a portal eithe harness null a statement about `linux-x64`: it ran on arm64, and a codegen hypothesis is architecture-specific. +#### The defect, in one listing — the JIT drops the `isinst`, and Tiered PGO off removes it + +The capture above was finally read on core, where a workload change made the condition frequent: +the class split that #6128 shifted moved `InstallNeverInheritsRepoCompileVerdictTest` into +`Memex.Portal.Shared.Test` part 1/2, behind the stand-in emits of `ModulePlatformMemberLinkTest`, +`ModuleLinkVersionTest` and friends. From then on that part poisoned in **4 of 12** `main` runs +(`InstallNeverInherits…` and both `ModulesUpdateIndependentlyOfThePlatformTest` cases, or the whole of +`ModuleLinkVersionTest` when the onset came earlier) — no diff in those pull requests was involved. + +**The reproduction in CI's own shape** (experiment branch `exp/890-tieredpgo-split-arm`, run +[`37647154163`](https://github.com/Systemorph/MeshWeaver/actions/runs/37647154163)): the very +`build-output-3` artifact of a poisoned pull-request run, part 1/2 launched exactly as the shard launches +it (`dotnet Memex.Portal.Shared.Test.dll -class …`), 14 runners per arm, on whatever `ubuntu-latest` hosts +GitHub assigned (EPYC 7763 without AVX-512, EPYC 9V74/9V45 and Xeon with it): + +| arm | poisoned | clean | +|---|---|---| +| defaults + `DOTNET_JitDisasm` capture | **4** | 10 | +| `DOTNET_TieredPGO=0` | **0** | 14 | + +**And the mechanism, 14 of 14.** The Tier-1 listing of +`NamedTypeSymbol:AsNestedTypeDefinitionImpl` inlines `SourceMemberContainerTypeSymbol.ContainingType` +(`_containingSymbol as NamedTypeSymbol`) behind a guarded devirtualisation. In all **10** clean hosts the +inlined body keeps the cast — a likely-class compare, then `CORINFO_HELP_ISINSTANCEOFCLASS`. In all **4** +poisoned hosts it is gone: + +```text +G_M000_IG07: + mov rax, gword ptr [rbx+0x38] ; _containingSymbol, NO isinst + jmp SHORT G_M000_IG04 ; → test rax, rax / jne: "is nested" +``` + +So for a top-level type the namespace container reads as a non-null containing type, the guard admits +it, and `get_ContainingTypeDefinition` — whose own, separately compiled `ContainingType` still casts — +returns null and the writer dereferences it. That one listing explains every reading this page +collected: `dissect=READS-HEALTHY` (a direct call goes through the correct standalone method), +`flat=SAME-FRAME` (one member-less top-level class is enough), `compiler=PRIVATE-COPY-EMITS` (a cold copy +runs tier-0 code), the acquired onset (the method reaches tier 1 after enough emits) and the per-process +lottery (which code tier 1 produces depends on the profile that process collected). It is independent +of the ISA (both VEX and EVEX hosts poisoned). The standalone `ContainingType` in a poisoned host still +casts, so this is the inliner's copy only. *Why* the cast is folded is not established — the listing +shows the result, not the JIT phase; the profile-driven cast expansion is the suspect, because the +dropped cast and the surviving one differ only in which PGO data fed them. + +**The remedy is the supported runtime switch, at the narrowest place that covers every compiling host:** +`false` in the root `Directory.Build.props` (every executable under `src/`, +`tools/` and `memex/`, including the bake host `mw-plugin-test`) and in `test/Directory.Build.props` +(which does not import the root). It reaches the runtime as `System.Runtime.TieredPGO: false` in each +host's runtimeconfig — the file a `dotnet .dll` launch reads, which is how CI's shards start +hosts. `TieredPgoIsOffInEveryCompilingHostTest` pins that it arrives (negative control: a build with +`-p:TieredPGO=true` fails both of its assertions). This is a workaround for a runtime defect, stated as +one: the defect is in the .NET 10 JIT, not in this repository, and the switch is removed only with a +measurement on a runtime that fixes it — the arm above, re-run. The portal hosts live in +MeshWeaver.Plugins and need the same property there. + ### Framework-version freezing A compiled NodeType DLL references the MeshWeaver framework assemblies present diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 9d58460d2a..7d695a9904 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -26,6 +26,12 @@ the central src policy here so the test tree tracks it uniformly. Vulnerability handling (bump/replace) is a deliberate dependency decision, not a per-build -warnaserror gate. --> NU1901;NU1902;NU1903;NU1904;$(WarningsNotAsErrors) + + false + same property in test/Directory.Build.props, which does not import this file; + BakeHostRunsWithTieredPgoOffTest pins that this one reaches mw-plugin-test. --> false false