From 7f75c3c495d241363659956252c6693227fb6a9d Mon Sep 17 00:00:00 2001 From: wallstop Date: Sat, 15 Aug 2026 16:27:14 +0000 Subject: [PATCH 1/8] test(perf): harden deregistration attribution evidence --- .github/workflows/perf-numbers.yml | 303 ++++++++++++++++-- .../DispatchThroughputBenchmarks.cs | 34 +- ...strationLifecycleBenchmarkContractTests.cs | 236 ++++++++++++++ .../RegistrationLifecycleBenchmarks.cs | 169 ++++++++++ docs/runbooks/perf-benchmark-methodology.md | 14 + scripts/__tests__/unity-perf.test.js | 10 +- 6 files changed, 732 insertions(+), 34 deletions(-) diff --git a/.github/workflows/perf-numbers.yml b/.github/workflows/perf-numbers.yml index 6b540de2..648aae24 100644 --- a/.github/workflows/perf-numbers.yml +++ b/.github/workflows/perf-numbers.yml @@ -602,6 +602,7 @@ jobs: Set-Content -LiteralPath (Join-Path $artifactsPath 'player-size.txt') -Encoding utf8 Write-Host "Exact standalone player size: $totalBytes total bytes; $shippableBytes shippable bytes." + Set-StrictMode -Version Latest $beeArtifacts = Join-Path $projectPath 'Library\Bee\artifacts' $il2cppRoots = @( Get-ChildItem ` @@ -618,55 +619,301 @@ jobs: Sort-Object -Property LastWriteTimeUtc -Descending | Select-Object -First 1 $cppFiles = @( - Get-ChildItem -LiteralPath $il2cppRoot.FullName -File -Recurse -Filter '*.cpp' + Get-ChildItem -LiteralPath $il2cppRoot.FullName -File -Recurse -Filter '*.cpp' | + Sort-Object -Property FullName ) if ($cppFiles.Count -eq 0) { throw "No generated C++ files were found under $($il2cppRoot.FullName)." } - $codegenPatterns = @( - 'TypedHandlerDeregistrationState', - 'DeregisterTypedCache', - 'DeregisterErasedCache', - 'HandlerActionCache', - 'FastHandlerWithContext' - ) $cppPaths = @($cppFiles | ForEach-Object { $_.FullName }) - $codegenMatches = @( - foreach ($pattern in $codegenPatterns) { - Select-String -LiteralPath $cppPaths -Pattern $pattern -SimpleMatch -Context 1, 1 | - Select-Object -First 48 + # cspell:ignore gshared + function Get-GeneratedMethodDefinition { + param( + [Parameter(Mandatory)] [string] $Label, + [Parameter(Mandatory)] [string] $Pattern + ) + + $signatureMatches = @( + Select-String ` + -LiteralPath $cppPaths ` + -Pattern $Pattern + ) + $bodyCandidates = [System.Collections.Generic.List[object]]::new() + foreach ($signature in $signatureMatches) { + $fileLines = @(Get-Content -LiteralPath $signature.Path) + $startIndex = $signature.LineNumber - 1 + $openingBraceIndex = $startIndex + $openingBraceOnSignature = $fileLines[$openingBraceIndex] -match '\{' + if (!$openingBraceOnSignature) { + $openingBraceIndex++ + while ( + $openingBraceIndex -lt $fileLines.Count -and + [string]::IsNullOrWhiteSpace($fileLines[$openingBraceIndex]) + ) { + $openingBraceIndex++ + } + } + if ( + $openingBraceIndex -ge $fileLines.Count -or + (!$openingBraceOnSignature -and $fileLines[$openingBraceIndex].Trim() -ne '{') + ) { + continue + } + + $braceDepth = 0 + $endIndex = -1 + for ($index = $openingBraceIndex; $index -lt $fileLines.Count; $index++) { + $braceDepth += [regex]::Matches($fileLines[$index], '\{').Count + $braceDepth -= [regex]::Matches($fileLines[$index], '\}').Count + if ($braceDepth -eq 0) { + $endIndex = $index + break + } + if ($braceDepth -lt 0) { + break + } + } + if ($endIndex -lt $openingBraceIndex) { + throw "Generated definition for $Label had unbalanced braces." + } + $bodyCandidates.Add( + [pscustomobject]@{ + Label = $Label + Path = $signature.Path + LineNumber = $signature.LineNumber + Lines = @($fileLines[$startIndex..$endIndex]) + } + ) + } + if ($bodyCandidates.Count -ne 1) { + throw "Expected one generated body for $Label, found $($bodyCandidates.Count) from $($signatureMatches.Count) signatures." + } + return $bodyCandidates[0] + } + + $productionCppPaths = $cppPaths + $extractorTestRoot = Join-Path ( + [System.IO.Path]::GetTempPath() + ) ("dxm-codegen-extractor-{0}" -f [guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $extractorTestRoot | Out-Null + try { + $singleBodyPath = Join-Path $extractorTestRoot 'single.cpp' + $singleBodyLines = [System.Collections.Generic.List[string]]::new() + $singleBodyLines.Add('IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void Target_mABC () ;') + $singleBodyLines.Add('inline void PrefixTarget_mABC ()') + $singleBodyLines.Add('{') + $singleBodyLines.Add('}') + $singleBodyLines.Add('inline void Target_mABC ()') + $singleBodyLines.Add('{') + $singleBodyLines.Add('if (true)') + $singleBodyLines.Add('{') + $singleBodyLines.Add('}') + foreach ($unusedIndex in 0..170) { + $singleBodyLines.Add('// retained body line') + } + $singleBodyLines.Add('InterfaceActionInvoker IMessageBus') + $singleBodyLines.Add('}') + $singleBodyLines.Add('inline void Neighbor_mDEF ()') + $singleBodyLines.Add('{') + $singleBodyLines.Add('}') + $singleBodyLines | + Set-Content -LiteralPath $singleBodyPath -Encoding utf8 + $cppPaths = @($singleBodyPath) + $singleBody = Get-GeneratedMethodDefinition ` + -Label 'extractor self-test single body' ` + -Pattern '(?(); OrderAttribute deregistrationAttributionOrder = deregistrationAttributionMethod.GetCustomAttribute(); + OrderAttribute deregistrationDiagnosticOrder = + deregistrationDiagnosticMethod.GetCustomAttribute(); + OrderAttribute dispatchDiagnosticOrder = + dispatchDiagnosticMethod.GetCustomAttribute(); Assert.IsNotNull(dispatchOrder, "Published dispatch entry must declare its order."); Assert.IsNotNull( registrationAttributionOrder, @@ -327,6 +497,14 @@ public void PublishedDispatchAndAttributionUseSameSupportedMethodOrderBoundary() deregistrationAttributionOrder, "Deregistration attribution entry must declare its order." ); + Assert.IsNotNull( + deregistrationDiagnosticOrder, + "Deregistration palindrome must declare its order." + ); + Assert.IsNotNull( + dispatchDiagnosticOrder, + "Dispatch palindrome must declare its order." + ); Assert.AreEqual( DispatchThroughputBenchmarks.PublishedDispatchOrder, dispatchOrder.Order, @@ -352,6 +530,11 @@ public void PublishedDispatchAndAttributionUseSameSupportedMethodOrderBoundary() }, "Published dispatch and attribution rows must retain one deterministic boundary." ); + CollectionAssert.AreEqual( + new[] { 3, 4 }, + new[] { deregistrationDiagnosticOrder.Order, dispatchDiagnosticOrder.Order }, + "Diagnostic palindromes must run immediately after published attribution rows." + ); Type[] attributionTypes = { @@ -375,6 +558,59 @@ public void PublishedDispatchAndAttributionUseSameSupportedMethodOrderBoundary() ); } + private static IEnumerable DeregistrationPalindromeCases() + { + yield return PalindromeCase("Stable", 130d, 100d, 101d, 131d, 15); + yield return PalindromeCase("HandlerOnlyDrift", 101d, 1d, 1d, 104.04d, 6); + yield return PalindromeCase("BusOnlyDrift", 1100d, 100d, 103.1d, 1103.1d, 5); + yield return PalindromeCase("SamePathExactThreshold", 1100d, 100d, 103d, 1103d, 15); + yield return PalindromeCase( + "SamePathJustOverThreshold", + 1100d, + 100d, + 103.000001d, + 1103.000001d, + 5 + ); + yield return PalindromeCase("ExcessExactThreshold", 198.5d, 100d, 100d, 201.5d, 15); + yield return PalindromeCase( + "ExcessJustOverThreshold", + 198.5d, + 100d, + 100d, + 201.500001d, + 3 + ); + yield return PalindromeCase("ZeroDuration", 0d, 1d, 1d, 2d, 2); + yield return PalindromeCase("NegativeDuration", -1d, 1d, 1d, 2d, 2); + yield return PalindromeCase("ZeroExcess", 1d, 1d, 1d, 1d, 3); + yield return PalindromeCase("NegativeExcess", 1d, 2d, 2d, 1d, 3); + yield return PalindromeCase("NaNDuration", double.NaN, 1d, 1d, 2d, 2); + yield return PalindromeCase("InfiniteDuration", double.PositiveInfinity, 1d, 1d, 2d, 2); + yield return PalindromeCase( + "FiniteMeanOverflow", + double.MaxValue, + 1d, + 1d, + double.MaxValue, + 15 + ); + } + + private static TestCaseData PalindromeCase( + string name, + double handlerA, + double busA, + double busB, + double handlerB, + int expectedClassification + ) + { + return new TestCaseData(handlerA, busA, busB, handlerB, expectedClassification).SetName( + $"DeregistrationPalindrome_{name}" + ); + } + private static IEnumerable LifecycleOperationCases() { const int Cardinality = 4; diff --git a/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarks.cs b/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarks.cs index 96a067bd..df1ed66b 100644 --- a/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarks.cs +++ b/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarks.cs @@ -946,6 +946,8 @@ public enum DeregistrationAttributionOperation internal static class DeregistrationAttributionBenchmarks { internal const int Cardinality = 131_072; + internal const double MaxSamePathDriftPercent = 3d; + internal const double MaxHandlerExcessSpreadPercent = 3d; private const int TimingTrials = 7; internal static DeregistrationAttributionObservation ExecuteOnceForContract( @@ -1013,6 +1015,21 @@ internal static string ScenarioKey(DeregistrationAttributionOperation operation) }; } + internal static DeregistrationAttributionPalindromeDiagnostic AnalyzePalindrome( + double handlerA, + double busA, + double busB, + double handlerB + ) + { + return new DeregistrationAttributionPalindromeDiagnostic( + handlerA, + busA, + busB, + handlerB + ); + } + private static void ValidateCardinality(int cardinality) { if (cardinality <= 0) @@ -1272,6 +1289,158 @@ public void Dispose() } } + internal readonly struct DeregistrationAttributionPalindromeDiagnostic + { + public DeregistrationAttributionPalindromeDiagnostic( + double handlerA, + double busA, + double busB, + double handlerB + ) + { + HandlerA = handlerA; + BusA = busA; + BusB = busB; + HandlerB = handlerB; + } + + public double HandlerA { get; } + + public double BusA { get; } + + public double BusB { get; } + + public double HandlerB { get; } + + public double HandlerExcessA => HandlerA - BusA; + + public double HandlerExcessB => HandlerB - BusB; + + public double CenteredHandlerExcess => SafePositiveMean(HandlerExcessA, HandlerExcessB); + + public double HandlerDriftPercent => RelativeDriftPercent(HandlerA, HandlerB); + + public double BusDriftPercent => RelativeDriftPercent(BusA, BusB); + + public double HandlerExcessSpreadPercent => + HasFinitePositiveExcesses + ? Math.Abs(HandlerExcessB - HandlerExcessA) / CenteredHandlerExcess * 100d + : double.PositiveInfinity; + + public bool HandlerDriftWithinThreshold => + WithinSymmetricThreshold( + HandlerA, + HandlerB, + DeregistrationAttributionBenchmarks.MaxSamePathDriftPercent + ); + + public bool BusDriftWithinThreshold => + WithinSymmetricThreshold( + BusA, + BusB, + DeregistrationAttributionBenchmarks.MaxSamePathDriftPercent + ); + + public bool HandlerExcessSpreadWithinThreshold => + HasFinitePositiveExcesses + && Math.Abs(HandlerExcessB - HandlerExcessA) + <= CenteredHandlerExcess + * (DeregistrationAttributionBenchmarks.MaxHandlerExcessSpreadPercent / 100d); + + public bool HasFinitePositiveDurations => + IsFinitePositive(HandlerA) + && IsFinitePositive(BusA) + && IsFinitePositive(BusB) + && IsFinitePositive(HandlerB); + + public bool HasFinitePositiveExcesses => + IsFinitePositive(HandlerExcessA) + && IsFinitePositive(HandlerExcessB) + && IsFinitePositive(CenteredHandlerExcess); + + public bool Interpretable => + HasFinitePositiveDurations + && HasFinitePositiveExcesses + && IsFiniteNonNegative(HandlerDriftPercent) + && IsFiniteNonNegative(BusDriftPercent) + && IsFiniteNonNegative(HandlerExcessSpreadPercent) + && HandlerDriftWithinThreshold + && BusDriftWithinThreshold + && HandlerExcessSpreadWithinThreshold; + + public string ToStructuredLog() + { + return "DXM_DEREGISTRATION_ATTRIBUTION_PALINDROME " + + $"handlerA_ms={Format(HandlerA)} busA_ms={Format(BusA)} " + + $"busB_ms={Format(BusB)} handlerB_ms={Format(HandlerB)} " + + $"handlerExcessA_ms={Format(HandlerExcessA)} " + + $"handlerExcessB_ms={Format(HandlerExcessB)} " + + $"centeredHandlerExcess_ms={Format(CenteredHandlerExcess)} " + + $"handlerDriftPercent={Format(HandlerDriftPercent)} " + + $"busDriftPercent={Format(BusDriftPercent)} " + + $"handlerExcessSpreadPercent={Format(HandlerExcessSpreadPercent)} " + + $"handlerDriftWithinThreshold={Format(HandlerDriftWithinThreshold)} " + + $"busDriftWithinThreshold={Format(BusDriftWithinThreshold)} " + + $"handlerExcessSpreadWithinThreshold={Format(HandlerExcessSpreadWithinThreshold)} " + + $"maxSamePathDriftPercent={Format(DeregistrationAttributionBenchmarks.MaxSamePathDriftPercent)} " + + $"maxHandlerExcessSpreadPercent={Format(DeregistrationAttributionBenchmarks.MaxHandlerExcessSpreadPercent)} " + + $"finitePositiveDurations={Format(HasFinitePositiveDurations)} " + + $"finitePositiveExcesses={Format(HasFinitePositiveExcesses)} " + + $"independentMinima=true diagnosticOnly=true acceptanceEvidence=false " + + $"candidateCompared=false interpretable={Format(Interpretable)}"; + } + + private static string Format(double value) => + value.ToString("R", CultureInfo.InvariantCulture); + + private static string Format(bool value) => value ? "true" : "false"; + + private static double RelativeDriftPercent(double first, double second) + { + if (!IsFinitePositive(first) || !IsFinitePositive(second)) + { + return double.PositiveInfinity; + } + + double minimum = Math.Min(first, second); + double maximum = Math.Max(first, second); + return (maximum - minimum) / minimum * 100d; + } + + private static bool WithinSymmetricThreshold( + double first, + double second, + double thresholdPercent + ) + { + if (!IsFinitePositive(first) || !IsFinitePositive(second)) + { + return false; + } + + double minimum = Math.Min(first, second); + double maximum = Math.Max(first, second); + return maximum - minimum <= minimum * (thresholdPercent / 100d); + } + + private static double SafePositiveMean(double first, double second) + { + if (!IsFinitePositive(first) || !IsFinitePositive(second)) + { + return double.PositiveInfinity; + } + + double minimum = Math.Min(first, second); + return minimum + (Math.Max(first, second) - minimum) / 2d; + } + + private static bool IsFinitePositive(double value) => + value > 0d && !double.IsInfinity(value) && !double.IsNaN(value); + + private static bool IsFiniteNonNegative(double value) => + value >= 0d && !double.IsInfinity(value) && !double.IsNaN(value); + } + public readonly struct DeregistrationAttributionObservation { public DeregistrationAttributionObservation( diff --git a/docs/runbooks/perf-benchmark-methodology.md b/docs/runbooks/perf-benchmark-methodology.md index 7fd0898a..c397523f 100644 --- a/docs/runbooks/perf-benchmark-methodology.md +++ b/docs/runbooks/perf-benchmark-methodology.md @@ -172,6 +172,20 @@ they are report-only -- rendered as wall clock, never gated. scenario before the 131072-cycle and retained-population attribution setup. These diagnostic wall-clock rows are not hard regression gates. Do not compare them with the 1000-type flood rows because their registration topology differs. +- **Deregistration attribution palindrome.** The non-published + `DirectHandlerAndBusDeregistrationPalindromeDiagnostic` runs direct handler A, + direct bus A, direct bus B, then direct handler B after the published + attribution rows. It reports + `DXM_DEREGISTRATION_ATTRIBUTION_PALINDROME` with both additive handler-minus-bus + excesses and their arithmetic center. Interpret the sample only when both + excesses are positive, handler and bus same-path drift are each at most 3%, and + the two additive excesses differ by at most 3% of their center. The four arms + independently select the minimum of seven fresh populations, so they do not + preserve cross-path covariance. `interpretable=true` is a noise-rejection + prerequisite, not candidate acceptance. The marker always records + `diagnosticOnly=true`, `acceptanceEvidence=false`, and + `candidateCompared=false`; require a separate repeated control/candidate + bracket before claiming a 3% improvement. - **Noise control on the wall-clock floods.** A single one-shot sample of a ~1 ms operation on a shared CI runner swings run-to-run by tens of percent (scheduler preemption, or a GC landing inside the timed window). Two mitigations: (1) the diff --git a/scripts/__tests__/unity-perf.test.js b/scripts/__tests__/unity-perf.test.js index 1d66599e..8a79e7e6 100644 --- a/scripts/__tests__/unity-perf.test.js +++ b/scripts/__tests__/unity-perf.test.js @@ -518,17 +518,17 @@ test("render-perf-deltas CLI failures preserve non-gating diagnostic output", () assert.match(result.stderr, /workflow decides whether the regressed= signal fails CI/); }); -test("performance workflow publishes an exact IL2CPP player-size manifest", () => { +test("performance workflow publishes exact player-size and codegen evidence", () => { const workflow = fs.readFileSync( path.join(REPO_ROOT, ".github", "workflows", "perf-numbers.yml"), "utf8" ); - assert.match(workflow, /name: Capture exact standalone player size/); - assert.match(workflow, /matrix\.test-mode == 'standalone'/); assert.match(workflow, /Get-ChildItem -LiteralPath \$playerDir -File -Recurse -Force/); - for (const marker of ["shippableBytes", "typed-deregistration-codegen.txt"]) - assert.ok(workflow.includes(marker)); + assert.match( + workflow, + /Generated-method extractor self-test passed[\s\S]*DeregistrationAttributionState_Execute_m[\s\S]*TypedHandlerDeregistrationState_Deregister_m[\s\S]*MessageBus_Deregister_Tis[\s\S]*typedCallSymbol[\s\S]*typedSharedCallSymbol[\s\S]*directBusCallSymbol[\s\S]*typedInterfaceDispatchEvidenceLineCount[\s\S]*typedDirectBusDispatchEvidenceLineCount[\s\S]*typedBusDispatchRecognized[\s\S]*directBusDispatchEvidenceLineCount[\s\S]*typed-deregistration-codegen\.txt/ + ); assert.match(workflow, /player-size\.json/); }); From 40ad6c88a5fb9d394eea0a791f0dcc19c4d89f4b Mon Sep 17 00:00:00 2001 From: wallstop Date: Sat, 15 Aug 2026 16:44:16 +0000 Subject: [PATCH 2/8] fix(ci): accept repeated inline codegen bodies --- .github/workflows/perf-numbers.yml | 73 +++++++++++++++++++++++++--- scripts/__tests__/unity-perf.test.js | 2 +- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/.github/workflows/perf-numbers.yml b/.github/workflows/perf-numbers.yml index 648aae24..256adcf3 100644 --- a/.github/workflows/perf-numbers.yml +++ b/.github/workflows/perf-numbers.yml @@ -685,10 +685,35 @@ jobs: } ) } - if ($bodyCandidates.Count -ne 1) { - throw "Expected one generated body for $Label, found $($bodyCandidates.Count) from $($signatureMatches.Count) signatures." + $uniqueBodyKeys = @( + $bodyCandidates | + ForEach-Object { $_.Lines -join "`n" } | + Sort-Object -Unique + ) + if ($uniqueBodyKeys.Count -ne 1) { + $bodyCountMessage = + "Expected one unique generated body for $Label, " + + "found $($uniqueBodyKeys.Count) unique bodies across " + + "$($bodyCandidates.Count) definitions from " + + "$($signatureMatches.Count) signatures." + throw $bodyCountMessage } - return $bodyCandidates[0] + $canonicalBody = $bodyCandidates[0] + $canonicalBody | Add-Member ` + -NotePropertyName DefinitionOccurrenceCount ` + -NotePropertyValue $bodyCandidates.Count + $canonicalBody | Add-Member ` + -NotePropertyName DefinitionLocations ` + -NotePropertyValue @( + $bodyCandidates | + ForEach-Object { + [pscustomobject]@{ + Path = $_.Path + LineNumber = $_.LineNumber + } + } + ) + return $canonicalBody } $productionCppPaths = $cppPaths @@ -724,19 +749,40 @@ jobs: -Pattern '(? Date: Sat, 15 Aug 2026 16:58:28 +0000 Subject: [PATCH 3/8] fix(ci): recognize RGCTX message bus dispatch --- .github/workflows/perf-numbers.yml | 13 +++++++++---- scripts/__tests__/unity-perf.test.js | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/perf-numbers.yml b/.github/workflows/perf-numbers.yml index 256adcf3..68253f39 100644 --- a/.github/workflows/perf-numbers.yml +++ b/.github/workflows/perf-numbers.yml @@ -899,14 +899,19 @@ jobs: for ($index = 0; $index -lt $typedHandlerSharedBody.Lines.Count; $index++) { if ( $typedHandlerSharedBody.Lines[$index] -notmatch - 'InterfaceActionInvoker|InterfaceFuncInvoker|VirtualActionInvoker|GetInterfaceInvokeData' + 'InterfaceActionInvoker|InterfaceFuncInvoker|GenericInterfaceActionInvoker|GenericInterfaceFuncInvoker|VirtualActionInvoker|GetInterfaceInvokeData' ) { continue } - $windowStart = [Math]::Max(0, $index - 3) - $windowEnd = [Math]::Min($typedHandlerSharedBody.Lines.Count - 1, $index + 3) + $windowStart = [Math]::Max(0, $index - 6) + $windowEnd = [Math]::Min($typedHandlerSharedBody.Lines.Count - 1, $index + 6) $dispatchWindow = $typedHandlerSharedBody.Lines[$windowStart..$windowEnd] -join ' ' - if ($dispatchWindow -match 'IMessageBus') { + $explicitIMessageBusDispatch = $dispatchWindow -match 'IMessageBus' + $rgctxMessageBusDispatch = + $typedHandlerSharedBody.Lines[$index] -match + 'GenericInterface(?:Action|Func)Invoker' -and + $dispatchWindow -match '_+messageBus' + if ($explicitIMessageBusDispatch -or $rgctxMessageBusDispatch) { $typedInterfaceDispatchEvidence.Add($typedHandlerSharedBody.Lines[$index]) } } diff --git a/scripts/__tests__/unity-perf.test.js b/scripts/__tests__/unity-perf.test.js index faa38c64..093ba1c0 100644 --- a/scripts/__tests__/unity-perf.test.js +++ b/scripts/__tests__/unity-perf.test.js @@ -527,7 +527,7 @@ test("performance workflow publishes exact player-size and codegen evidence", () assert.match(workflow, /Get-ChildItem -LiteralPath \$playerDir -File -Recurse -Force/); assert.match( workflow, - /repeated identical body[\s\S]*Generated-method extractor self-test passed[\s\S]*DeregistrationAttributionState_Execute_m[\s\S]*TypedHandlerDeregistrationState_Deregister_m[\s\S]*MessageBus_Deregister_Tis[\s\S]*typedCallSymbol[\s\S]*typedSharedCallSymbol[\s\S]*directBusCallSymbol[\s\S]*typedInterfaceDispatchEvidenceLineCount[\s\S]*typedDirectBusDispatchEvidenceLineCount[\s\S]*typedBusDispatchRecognized[\s\S]*directBusDispatchEvidenceLineCount[\s\S]*definitionOccurrenceCount[\s\S]*typed-deregistration-codegen\.txt/ + /repeated identical body[\s\S]*Generated-method extractor self-test passed[\s\S]*DeregistrationAttributionState_Execute_m[\s\S]*TypedHandlerDeregistrationState_Deregister_m[\s\S]*MessageBus_Deregister_Tis[\s\S]*GenericInterface[\s\S]*_\+messageBus[\s\S]*typedCallSymbol[\s\S]*typedSharedCallSymbol[\s\S]*directBusCallSymbol[\s\S]*typedInterfaceDispatchEvidenceLineCount[\s\S]*typedDirectBusDispatchEvidenceLineCount[\s\S]*typedBusDispatchRecognized[\s\S]*directBusDispatchEvidenceLineCount[\s\S]*definitionOccurrenceCount[\s\S]*typed-deregistration-codegen\.txt/ ); assert.match(workflow, /player-size\.json/); }); From 0f382dd27bed34b915b14375fa2d2022adbe7f6d Mon Sep 17 00:00:00 2001 From: wallstop Date: Sat, 15 Aug 2026 17:00:49 +0000 Subject: [PATCH 4/8] chore(ci): allow IL2CPP RGCTX spelling --- .github/workflows/perf-numbers.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/perf-numbers.yml b/.github/workflows/perf-numbers.yml index 68253f39..ebbed75f 100644 --- a/.github/workflows/perf-numbers.yml +++ b/.github/workflows/perf-numbers.yml @@ -626,7 +626,7 @@ jobs: throw "No generated C++ files were found under $($il2cppRoot.FullName)." } $cppPaths = @($cppFiles | ForEach-Object { $_.FullName }) - # cspell:ignore gshared + # cspell:ignore gshared rgctx function Get-GeneratedMethodDefinition { param( [Parameter(Mandatory)] [string] $Label, From 1c4fbd3d6f2fd60a35d70506f664978bb5594333 Mon Sep 17 00:00:00 2001 From: wallstop Date: Sat, 15 Aug 2026 17:58:24 +0000 Subject: [PATCH 5/8] fix(ci): compare codegen bodies case-sensitively --- .github/workflows/perf-numbers.yml | 6 +++--- scripts/__tests__/unity-perf.test.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/perf-numbers.yml b/.github/workflows/perf-numbers.yml index ebbed75f..b6ef16ec 100644 --- a/.github/workflows/perf-numbers.yml +++ b/.github/workflows/perf-numbers.yml @@ -688,7 +688,7 @@ jobs: $uniqueBodyKeys = @( $bodyCandidates | ForEach-Object { $_.Lines -join "`n" } | - Sort-Object -Unique + Sort-Object -CaseSensitive -Unique ) if ($uniqueBodyKeys.Count -ne 1) { $bodyCountMessage = @@ -792,11 +792,11 @@ jobs: @( 'inline void Duplicate_m123 ()', '{', - '// first body', + '// case-sensitive body', '}', 'inline void Duplicate_m123 ()', '{', - '// second body', + '// Case-sensitive body', '}' ) | Set-Content -LiteralPath $multipleBodyPath -Encoding utf8 $cppPaths = @($multipleBodyPath) diff --git a/scripts/__tests__/unity-perf.test.js b/scripts/__tests__/unity-perf.test.js index 093ba1c0..750428cc 100644 --- a/scripts/__tests__/unity-perf.test.js +++ b/scripts/__tests__/unity-perf.test.js @@ -527,7 +527,7 @@ test("performance workflow publishes exact player-size and codegen evidence", () assert.match(workflow, /Get-ChildItem -LiteralPath \$playerDir -File -Recurse -Force/); assert.match( workflow, - /repeated identical body[\s\S]*Generated-method extractor self-test passed[\s\S]*DeregistrationAttributionState_Execute_m[\s\S]*TypedHandlerDeregistrationState_Deregister_m[\s\S]*MessageBus_Deregister_Tis[\s\S]*GenericInterface[\s\S]*_\+messageBus[\s\S]*typedCallSymbol[\s\S]*typedSharedCallSymbol[\s\S]*directBusCallSymbol[\s\S]*typedInterfaceDispatchEvidenceLineCount[\s\S]*typedDirectBusDispatchEvidenceLineCount[\s\S]*typedBusDispatchRecognized[\s\S]*directBusDispatchEvidenceLineCount[\s\S]*definitionOccurrenceCount[\s\S]*typed-deregistration-codegen\.txt/ + /Sort-Object -CaseSensitive -Unique[\s\S]*repeated identical body[\s\S]*Generated-method extractor self-test passed[\s\S]*DeregistrationAttributionState_Execute_m[\s\S]*TypedHandlerDeregistrationState_Deregister_m[\s\S]*MessageBus_Deregister_Tis[\s\S]*GenericInterface[\s\S]*_\+messageBus[\s\S]*typedCallSymbol[\s\S]*typedSharedCallSymbol[\s\S]*directBusCallSymbol[\s\S]*typedInterfaceDispatchEvidenceLineCount[\s\S]*typedDirectBusDispatchEvidenceLineCount[\s\S]*typedBusDispatchRecognized[\s\S]*directBusDispatchEvidenceLineCount[\s\S]*definitionOccurrenceCount[\s\S]*typed-deregistration-codegen\.txt/ ); assert.match(workflow, /player-size\.json/); }); From 369fe0d59171e84b694330aa893f99cd73618208 Mon Sep 17 00:00:00 2001 From: wallstop Date: Sat, 15 Aug 2026 18:00:48 +0000 Subject: [PATCH 6/8] test(ci): pin codegen location evidence --- .github/workflows/perf-numbers.yml | 5 ++++- scripts/__tests__/unity-perf.test.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/perf-numbers.yml b/.github/workflows/perf-numbers.yml index b6ef16ec..88404273 100644 --- a/.github/workflows/perf-numbers.yml +++ b/.github/workflows/perf-numbers.yml @@ -770,7 +770,10 @@ jobs: $repeatedBody = Get-GeneratedMethodDefinition ` -Label 'extractor self-test repeated identical body' ` -Pattern '(? Date: Sat, 15 Aug 2026 18:03:42 +0000 Subject: [PATCH 7/8] fix(ci): compare codegen bodies ordinally --- .github/workflows/perf-numbers.yml | 26 +++++++++++++++++++------- scripts/__tests__/unity-perf.test.js | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.github/workflows/perf-numbers.yml b/.github/workflows/perf-numbers.yml index 88404273..31077bd0 100644 --- a/.github/workflows/perf-numbers.yml +++ b/.github/workflows/perf-numbers.yml @@ -685,11 +685,12 @@ jobs: } ) } - $uniqueBodyKeys = @( - $bodyCandidates | - ForEach-Object { $_.Lines -join "`n" } | - Sort-Object -CaseSensitive -Unique + $uniqueBodyKeys = [System.Collections.Generic.HashSet[string]]::new( + [System.StringComparer]::Ordinal ) + foreach ($bodyCandidate in $bodyCandidates) { + $null = $uniqueBodyKeys.Add($bodyCandidate.Lines -join "`n") + } if ($uniqueBodyKeys.Count -ne 1) { $bodyCountMessage = "Expected one unique generated body for $Label, " + @@ -792,14 +793,25 @@ jobs: } $multipleBodyPath = Join-Path $extractorTestRoot 'multiple.cpp' + $composedBodyMarker = [string][char]0x00E9 + $decomposedBodyMarker = "e$([char]0x0301)" + if ( + [string]::Equals( + $composedBodyMarker, + $decomposedBodyMarker, + [System.StringComparison]::Ordinal + ) + ) { + throw 'Generated-method extractor Unicode self-test inputs were not distinct.' + } @( 'inline void Duplicate_m123 ()', '{', - '// case-sensitive body', + "// $composedBodyMarker body", '}', 'inline void Duplicate_m123 ()', '{', - '// Case-sensitive body', + "// $decomposedBodyMarker body", '}' ) | Set-Content -LiteralPath $multipleBodyPath -Encoding utf8 $cppPaths = @($multipleBodyPath) @@ -814,7 +826,7 @@ jobs: 'found 2 unique bodies across 2 definitions from 2 signatures' } if (!$multipleRejected) { - throw 'Generated-method extractor did not reject multiple bodies.' + throw 'Generated-method extractor did not reject ordinally distinct bodies.' } Write-Host 'Generated-method extractor self-test passed.' } finally { diff --git a/scripts/__tests__/unity-perf.test.js b/scripts/__tests__/unity-perf.test.js index 36960756..ba577f70 100644 --- a/scripts/__tests__/unity-perf.test.js +++ b/scripts/__tests__/unity-perf.test.js @@ -527,7 +527,7 @@ test("performance workflow publishes exact player-size and codegen evidence", () assert.match(workflow, /Get-ChildItem -LiteralPath \$playerDir -File -Recurse -Force/); assert.match( workflow, - /Set-StrictMode -Version Latest[\s\S]*Sort-Object -CaseSensitive -Unique[\s\S]*DefinitionLocations[\s\S]*repeated identical body[\s\S]*Generated-method extractor self-test passed[\s\S]*DeregistrationAttributionState_Execute_m[\s\S]*TypedHandlerDeregistrationState_Deregister_m[\s\S]*MessageBus_Deregister_Tis[\s\S]*GenericInterface[\s\S]*_\+messageBus[\s\S]*typedCallSymbol[\s\S]*typedSharedCallSymbol[\s\S]*directBusCallSymbol[\s\S]*typedInterfaceDispatchEvidenceLineCount[\s\S]*typedDirectBusDispatchEvidenceLineCount[\s\S]*typedBusDispatchRecognized[\s\S]*directBusDispatchEvidenceLineCount[\s\S]*definitionOccurrenceCount[\s\S]*definitionLocation=[\s\S]*typed-deregistration-codegen\.txt/ + /Set-StrictMode -Version Latest[\s\S]*HashSet\[string\][\s\S]*StringComparer\]::Ordinal[\s\S]*DefinitionLocations[\s\S]*repeated identical body[\s\S]*StringComparison\]::Ordinal[\s\S]*Generated-method extractor self-test passed[\s\S]*DeregistrationAttributionState_Execute_m[\s\S]*TypedHandlerDeregistrationState_Deregister_m[\s\S]*MessageBus_Deregister_Tis[\s\S]*GenericInterface[\s\S]*_\+messageBus[\s\S]*typedCallSymbol[\s\S]*typedSharedCallSymbol[\s\S]*directBusCallSymbol[\s\S]*typedInterfaceDispatchEvidenceLineCount[\s\S]*typedDirectBusDispatchEvidenceLineCount[\s\S]*typedBusDispatchRecognized[\s\S]*directBusDispatchEvidenceLineCount[\s\S]*definitionOccurrenceCount[\s\S]*definitionLocation=[\s\S]*typed-deregistration-codegen\.txt/ ); assert.match(workflow, /player-size\.json/); }); From 8678a2ca6b72694ed07499041eb432100eabf9e9 Mon Sep 17 00:00:00 2001 From: wallstop Date: Sat, 15 Aug 2026 18:21:57 +0000 Subject: [PATCH 8/8] test(ci): reject case-insensitive body equality --- .github/workflows/perf-numbers.yml | 26 ++++++++++++++++++++++++++ scripts/__tests__/unity-perf.test.js | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/.github/workflows/perf-numbers.yml b/.github/workflows/perf-numbers.yml index 31077bd0..03a86a35 100644 --- a/.github/workflows/perf-numbers.yml +++ b/.github/workflows/perf-numbers.yml @@ -792,6 +792,32 @@ jobs: throw 'Generated-method extractor did not reject zero bodies.' } + $caseDistinctBodyPath = Join-Path $extractorTestRoot 'case-distinct.cpp' + @( + 'inline void CaseDuplicate_m124 ()', + '{', + '// Alpha body', + '}', + 'inline void CaseDuplicate_m124 ()', + '{', + '// alpha body', + '}' + ) | Set-Content -LiteralPath $caseDistinctBodyPath -Encoding utf8 + $cppPaths = @($caseDistinctBodyPath) + $caseDistinctRejected = $false + try { + $null = Get-GeneratedMethodDefinition ` + -Label 'extractor self-test case-distinct bodies' ` + -Pattern '(?