From 5e1adf6a48972b18a7351f2e245fe82a367d0c3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 07:05:15 +0000 Subject: [PATCH 01/10] Initial plan From 9d16443398120e86803807fe59d6c57f5cd197ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 07:33:17 +0000 Subject: [PATCH 02/10] [TrimmableTypeMap] switch package hash generation to xxhash64 Agent-Logs-Url: https://github.com/dotnet/android/sessions/425dd1fe-e6a4-43e0-b0a9-de6b1e343c11 Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 10 +++++----- .../Scanner/JavaPeerScannerTests.EdgeCases.cs | 2 +- .../Scanner/JavaPeerScannerTests.cs | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 08ee7cd337d..df847a0d657 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1331,7 +1331,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) /// /// Compute both JNI name and compat JNI name for a type without [Register] or component Name. - /// JNI name uses CRC64 hash of "namespace:assemblyName" for the package. + /// JNI name uses XxHash64 hash of "namespace:assemblyName" for the package. /// Compat JNI name uses the raw managed namespace (lowercased). /// If a declaring type has [Register], its JNI name is used as prefix for both. /// Generic backticks are replaced with _. @@ -1345,7 +1345,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) return (name, name); } - var packageName = GetCrc64PackageName (ns, index.AssemblyName); + var packageName = GetXxHash64PackageName (ns, index.AssemblyName); var jniName = $"{packageName}/{typeName}"; string compatName = ns.Length == 0 @@ -1360,7 +1360,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) /// registered JNI name or the outermost namespace. /// Matches JavaNativeTypeManager.ToJniName behavior: walks up declaring types /// and if a parent has [Register] or a component attribute JNI name, uses that - /// as prefix instead of computing CRC64 from the namespace. + /// as prefix instead of computing XxHash64 from the namespace. /// static (string typeName, string? parentJniName, string ns) ComputeTypeNameParts (TypeDefinition typeDef, AssemblyIndex index) { @@ -1465,7 +1465,7 @@ static void ParseConnectorDeclaringType (string? connector, out string declaring declaringAssemblyName = nextComma >= 0 ? rest.Substring (0, nextComma).Trim () : rest.Trim (); } - static string GetCrc64PackageName (string ns, string assemblyName) + static string GetXxHash64PackageName (string ns, string assemblyName) { // Only Mono.Android preserves the namespace directly if (assemblyName == "Mono.Android") { @@ -1473,7 +1473,7 @@ static string GetCrc64PackageName (string ns, string assemblyName) } var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); - var hash = System.IO.Hashing.Crc64.Hash (data); + var hash = System.IO.Hashing.XxHash64.Hash (data); return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index b1f96d6d320..6db3290c015 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -54,7 +54,7 @@ public void Scan_EmptyNamespace_Handled () [InlineData ("MyApp.UnnamedActivity")] [InlineData ("MyApp.UnregisteredClickListener")] [InlineData ("MyApp.UnregisteredExporter")] - public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName) + public void Scan_UnregisteredType_DiscoveredWithHashedName (string managedName) { Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index a3b725caecb..d9f47800e16 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -88,10 +88,10 @@ public void Scan_RegisterAttribute_DotFormat_NormalizedToSlashes () } [Theory] - [InlineData ("MyApp.PlainActivitySubclass", "crc64eb3df85c64aa1af6/PlainActivitySubclass")] - [InlineData ("MyApp.UnregisteredClickListener", "crc64eb3df85c64aa1af6/UnregisteredClickListener")] - [InlineData ("MyApp.UnregisteredExporter", "crc64eb3df85c64aa1af6/UnregisteredExporter")] - public void Scan_UnregisteredType_UsesCrc64PackageName (string managedName, string expectedJavaName) + [InlineData ("MyApp.PlainActivitySubclass", "crc6403e39dfcc696a727/PlainActivitySubclass")] + [InlineData ("MyApp.UnregisteredClickListener", "crc6403e39dfcc696a727/UnregisteredClickListener")] + [InlineData ("MyApp.UnregisteredExporter", "crc6403e39dfcc696a727/UnregisteredExporter")] + public void Scan_UnregisteredType_UsesHashedPackageName (string managedName, string expectedJavaName) { Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); } From 174f08f2268a1488146bf607fe0b200ad6e7abb7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 07:37:16 +0000 Subject: [PATCH 03/10] [TrimmableTypeMap] clarify hashed package helper naming Agent-Logs-Url: https://github.com/dotnet/android/sessions/425dd1fe-e6a4-43e0-b0a9-de6b1e343c11 Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index df847a0d657..3770defe435 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1345,7 +1345,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) return (name, name); } - var packageName = GetXxHash64PackageName (ns, index.AssemblyName); + var packageName = GetHashedPackageName (ns, index.AssemblyName); var jniName = $"{packageName}/{typeName}"; string compatName = ns.Length == 0 @@ -1465,7 +1465,7 @@ static void ParseConnectorDeclaringType (string? connector, out string declaring declaringAssemblyName = nextComma >= 0 ? rest.Substring (0, nextComma).Trim () : rest.Trim (); } - static string GetXxHash64PackageName (string ns, string assemblyName) + static string GetHashedPackageName (string ns, string assemblyName) { // Only Mono.Android preserves the namespace directly if (assemblyName == "Mono.Android") { @@ -1474,6 +1474,7 @@ static string GetXxHash64PackageName (string ns, string assemblyName) var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); var hash = System.IO.Hashing.XxHash64.Hash (data); + // Keep the historical package prefix for compatibility. return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; } From 10496782453f9246c2a6fb43f6eba53dae0040f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:54:27 +0000 Subject: [PATCH 04/10] [TrimmableTypeMap] switch generated package prefix to xx64 Agent-Logs-Url: https://github.com/dotnet/android/sessions/b04a9e6a-bf80-4ff1-8601-4078c145728e Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Generator/ManifestGenerator.cs | 16 ++++++++-------- .../Scanner/JavaPeerScanner.cs | 3 +-- .../ScannerComparisonTests.Helpers.cs | 6 +++--- .../ScannerComparisonTests.cs | 8 ++++---- .../Generator/TypeMapModelBuilderTests.cs | 4 ++-- .../Scanner/JavaPeerScannerTests.Behavior.cs | 2 +- .../Scanner/JavaPeerScannerTests.EdgeCases.cs | 4 ++-- .../Scanner/JavaPeerScannerTests.cs | 6 +++--- 8 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index e83e7120ff6..aa0ad3ea57b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -148,23 +148,23 @@ XDocument CreateDefaultManifest () /// /// Manifest templates may use compat JNI names (e.g., "android.apptests.App") - /// but the trimmable path generates JCWs with CRC-based names (e.g., "crc64.../App"). + /// but the trimmable path generates JCWs with hashed package names (e.g., "xx64.../App"). /// This method rewrites any compat name references to the actual JCW name so the /// Android runtime can find the class. /// void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers) { - // Build mapping: fully-qualified compat Java name → CRC Java name - var compatToCrc = new Dictionary (allPeers.Count, StringComparer.Ordinal); + // Build mapping: fully-qualified compat Java name → hashed Java name + var compatToHashed = new Dictionary (allPeers.Count, StringComparer.Ordinal); foreach (var peer in allPeers) { string javaName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); string compatName = JniSignatureHelper.JniNameToJavaName (peer.CompatJniName); if (javaName != compatName) { - compatToCrc [compatName] = javaName; + compatToHashed [compatName] = javaName; } } - if (compatToCrc.Count == 0) { + if (compatToHashed.Count == 0) { return; } @@ -173,7 +173,7 @@ void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers // - fully qualified ("com.example.app.MainActivity") // - relative to the manifest package, starting with '.' (".MainActivity") // - bare, with no '.' at all ("MainActivity"), also relative to the package - // Resolve to the fully-qualified form before the lookup, then write the CRC + // Resolve to the fully-qualified form before the lookup, then write the hashed // name back so duplicate detection later in the pipeline works correctly. var packageName = (string?) manifest.Attribute ("package") ?? ""; @@ -187,8 +187,8 @@ void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers continue; } var resolved = ManifestNameResolver.Resolve (nameAttr.Value, packageName); - if (compatToCrc.TryGetValue (resolved, out var crcName)) { - nameAttr.Value = crcName; + if (compatToHashed.TryGetValue (resolved, out var hashedName)) { + nameAttr.Value = hashedName; } } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 3770defe435..4af5dc8f503 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1474,8 +1474,7 @@ static string GetHashedPackageName (string ns, string assemblyName) var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); var hash = System.IO.Hashing.XxHash64.Hash (data); - // Keep the historical package prefix for compatibility. - return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; + return $"xx64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; } static string ExtractNamespace (string fullName) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs index 5014fe28a4c..c08c1b95f7e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs @@ -72,12 +72,12 @@ static string[]? AllUserTypesAssemblyPaths { } } - static string NormalizeCrc64 (string javaName) + static string NormalizeHashedPackageName (string javaName) { - if (javaName.StartsWith ("crc64", StringComparison.Ordinal)) { + if (javaName.StartsWith ("crc64", StringComparison.Ordinal) || javaName.StartsWith ("xx64", StringComparison.Ordinal)) { int slash = javaName.IndexOf ('/'); if (slash > 0) { - return "crc64.../" + javaName.Substring (slash + 1); + return "hash.../" + javaName.Substring (slash + 1); } } return javaName; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index fcfe51cb1df..1628af0140c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -115,8 +115,8 @@ public void ExactTypeMap_UserTypesFixture () var fixturePath = paths! [0]; var (legacy, _) = ScannerRunner.RunLegacy (fixturePath); var (newEntries, _) = ScannerRunner.RunNew (paths); -var legacyNormalized = legacy.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); -var newNormalized = newEntries.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList (); +var legacyNormalized = legacy.Select (e => e with { JavaName = NormalizeHashedPackageName (e.JavaName) }).ToList (); +var newNormalized = newEntries.Select (e => e with { JavaName = NormalizeHashedPackageName (e.JavaName) }).ToList (); AssertTypeMapMatch (legacyNormalized, newNormalized); } @@ -132,9 +132,9 @@ public void ExactMarshalMethods_UserTypesFixture () var (_, newMethods) = ScannerRunner.RunNew (paths); var legacyNormalized = legacyMethods -.ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); +.ToDictionary (kvp => NormalizeHashedPackageName (kvp.Key), kvp => kvp.Value); var newNormalized = newMethods -.ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value); +.ToDictionary (kvp => NormalizeHashedPackageName (kvp.Key), kvp => kvp.Value); var result = MarshalMethodDiffHelper.CompareUserTypeMarshalMethods (legacyNormalized, newNormalized); AssertNoDiffs ("MISSING from new scanner", result.Missing); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index bd9d1ab6b0d..1efe5f88b5b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -267,10 +267,10 @@ public void Build_PeerWithInvoker_CreatesProxy () [InlineData ("MyApp.UnregisteredExporter")] [InlineData ("MyApp.UnregisteredHelper")] [InlineData ("MyApp.DerivedFromComponentBase")] - public void Build_Crc64RenamedPeer_StoresFinalJavaNameOnProxy (string managedName) + public void Build_HashedRenamedPeer_StoresFinalJavaNameOnProxy (string managedName) { var peer = FindFixtureByManagedName (managedName); - Assert.StartsWith ("crc64", peer.JavaName); + Assert.StartsWith ("xx64", peer.JavaName); Assert.NotEqual (peer.CompatJniName, peer.JavaName); var model = BuildModel (new [] { peer }, "MyTypeMap"); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs index ec8ba3e7ed0..365cfaf4bef 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -114,7 +114,7 @@ public void Scan_CompatJniName (string javaName, string expectedCompat) public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace () { var unregistered = FindFixtureByManagedName ("MyApp.UnregisteredHelper"); - Assert.StartsWith ("crc64", unregistered.JavaName); + Assert.StartsWith ("xx64", unregistered.JavaName); Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index 6db3290c015..2a772cbe98e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -22,7 +22,7 @@ public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered () Assert.Equal ("android/app/Activity", baseType.BaseJavaName); var derived = FindFixtureByManagedName ("MyApp.DerivedFromComponentBase"); - Assert.StartsWith ("crc64", derived.JavaName); + Assert.StartsWith ("xx64", derived.JavaName); } [Theory] @@ -56,7 +56,7 @@ public void Scan_EmptyNamespace_Handled () [InlineData ("MyApp.UnregisteredExporter")] public void Scan_UnregisteredType_DiscoveredWithHashedName (string managedName) { - Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName); + Assert.StartsWith ("xx64", FindFixtureByManagedName (managedName).JavaName); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index d9f47800e16..81201d21439 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -88,9 +88,9 @@ public void Scan_RegisterAttribute_DotFormat_NormalizedToSlashes () } [Theory] - [InlineData ("MyApp.PlainActivitySubclass", "crc6403e39dfcc696a727/PlainActivitySubclass")] - [InlineData ("MyApp.UnregisteredClickListener", "crc6403e39dfcc696a727/UnregisteredClickListener")] - [InlineData ("MyApp.UnregisteredExporter", "crc6403e39dfcc696a727/UnregisteredExporter")] + [InlineData ("MyApp.PlainActivitySubclass", "xx6403e39dfcc696a727/PlainActivitySubclass")] + [InlineData ("MyApp.UnregisteredClickListener", "xx6403e39dfcc696a727/UnregisteredClickListener")] + [InlineData ("MyApp.UnregisteredExporter", "xx6403e39dfcc696a727/UnregisteredExporter")] public void Scan_UnregisteredType_UsesHashedPackageName (string managedName, string expectedJavaName) { Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); From ec9206763d6399febc7733147c8a16b4a255142c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:37:34 +0000 Subject: [PATCH 05/10] [TrimmableTypeMap] support AndroidPackageNamingPolicy with XxHash64 default Agent-Logs-Url: https://github.com/dotnet/android/sessions/a90e2abd-ca59-4b98-a498-64caa983b1d0 Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 3 + .../Scanner/JavaPeerScanner.cs | 61 ++++++++++++++++--- .../TrimmableTypeMapGenerator.cs | 9 +-- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 1 + .../Tasks/GenerateTrimmableTypeMap.cs | 4 +- .../Xamarin.Android.Common.props.in | 2 +- .../Generator/FixtureTestBase.cs | 20 ++++++ .../Scanner/JavaPeerScannerTests.cs | 16 +++++ 8 files changed, 103 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index 249bdc8def1..99399656317 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -5,6 +5,7 @@ $(TargetFrameworkNETStandard) enable Nullable + true Microsoft.Android.Sdk.TrimmableTypeMap true ..\..\product.snk @@ -18,6 +19,8 @@ + + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 4af5dc8f503..6759b46738d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -6,6 +6,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using Java.Interop.Tools.JavaCallableWrappers; namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// @@ -16,8 +17,19 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// public sealed class JavaPeerScanner : IDisposable { + enum HashedPackageNamingPolicy { + XxHash64, + LowercaseCrc64, + } + readonly Dictionary assemblyCache = new (StringComparer.Ordinal); readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new (); + readonly HashedPackageNamingPolicy packageNamingPolicy; + + public JavaPeerScanner (string? packageNamingPolicy = null) + { + this.packageNamingPolicy = ParsePackageNamingPolicy (packageNamingPolicy); + } /// /// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex. @@ -913,7 +925,7 @@ static string GetJavaAccess (MethodAttributes access) return registerJniName; } - // Fall back to already-scanned results (component-attributed or CRC64-computed peers) + // Fall back to already-scanned results (component-attributed or hashed-package peers) if (results.TryGetValue (baseTypeName, out var basePeer)) { return basePeer.JavaName; } @@ -1331,12 +1343,12 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) /// /// Compute both JNI name and compat JNI name for a type without [Register] or component Name. - /// JNI name uses XxHash64 hash of "namespace:assemblyName" for the package. + /// JNI name uses the selected package naming policy hash for "namespace:assemblyName". /// Compat JNI name uses the raw managed namespace (lowercased). /// If a declaring type has [Register], its JNI name is used as prefix for both. /// Generic backticks are replaced with _. /// - static (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index) + (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index) { var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); @@ -1360,7 +1372,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) /// registered JNI name or the outermost namespace. /// Matches JavaNativeTypeManager.ToJniName behavior: walks up declaring types /// and if a parent has [Register] or a component attribute JNI name, uses that - /// as prefix instead of computing XxHash64 from the namespace. + /// as prefix instead of computing hashed package names from the namespace. /// static (string typeName, string? parentJniName, string ns) ComputeTypeNameParts (TypeDefinition typeDef, AssemblyIndex index) { @@ -1465,16 +1477,51 @@ static void ParseConnectorDeclaringType (string? connector, out string declaring declaringAssemblyName = nextComma >= 0 ? rest.Substring (0, nextComma).Trim () : rest.Trim (); } - static string GetHashedPackageName (string ns, string assemblyName) + string GetHashedPackageName (string ns, string assemblyName) { // Only Mono.Android preserves the namespace directly if (assemblyName == "Mono.Android") { return ns.ToLowerInvariant ().Replace ('.', '/'); } - var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); + return packageNamingPolicy switch { + HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ToLegacyCrc64 (ns + ":" + assemblyName), + _ => "xx64" + ToXxHash64 (ns + ":" + assemblyName), + }; + } + + static HashedPackageNamingPolicy ParsePackageNamingPolicy (string? packageNamingPolicy) + { + if (string.Equals (packageNamingPolicy, "LowercaseCrc64", StringComparison.OrdinalIgnoreCase)) { + return HashedPackageNamingPolicy.LowercaseCrc64; + } + + return HashedPackageNamingPolicy.XxHash64; + } + + static string ToLegacyCrc64 (string value) + { + var data = System.Text.Encoding.UTF8.GetBytes (value); + var hash = Crc64Helper.Compute (data); + var buf = new char [hash.Length * 2]; + int i = 0; + foreach (var b in hash) { + buf [i++] = GetHexLowerChar (b >> 4); + buf [i++] = GetHexLowerChar (b & 0xF); + } + return new string (buf); + } + + static string ToXxHash64 (string value) + { + var data = System.Text.Encoding.UTF8.GetBytes (value); var hash = System.IO.Hashing.XxHash64.Hash (data); - return $"xx64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; + return BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant (); + } + + static char GetHexLowerChar (int value) + { + return (char) (value < 10 ? ('0' + value) : ('a' + value - 10)); } static string ExtractNamespace (string fullName) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 5d14490503a..d877a70b028 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -27,13 +27,14 @@ public TrimmableTypeMapResult Execute ( HashSet frameworkAssemblyNames, bool useSharedTypemapUniverse = false, ManifestConfig? manifestConfig = null, - XDocument? manifestTemplate = null) + XDocument? manifestTemplate = null, + string? packageNamingPolicy = null) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); _ = frameworkAssemblyNames ?? throw new ArgumentNullException (nameof (frameworkAssemblyNames)); - var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies); + var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy); if (allPeers.Count == 0) { logger.LogNoJavaPeerTypesFound (); return new TrimmableTypeMapResult ([], [], allPeers); @@ -104,9 +105,9 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []); } - (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies) + (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies, string? packageNamingPolicy) { - using var scanner = new JavaPeerScanner (); + using var scanner = new JavaPeerScanner (packageNamingPolicy); var peers = scanner.Scan (assemblies); var manifestInfo = scanner.ScanAssemblyManifestInfo (); logger.LogJavaPeerScanInfo (assemblies.Count, peers.Count); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 40a2c1519c3..9ab79fbc2b3 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -98,6 +98,7 @@ Debug="$(AndroidIncludeDebugSymbols)" NeedsInternet="$(AndroidNeedsInternetPermission)" EmbedAssemblies="$(EmbedAssembliesIntoApk)" + PackageNamingPolicy="$(AndroidPackageNamingPolicy)" ManifestPlaceholders="$(AndroidManifestPlaceholders)" CheckedBuild="$(_AndroidCheckedBuild)" ApplicationJavaClass="$(AndroidApplicationJavaClass)" diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index f80a14157f1..cb4d51021d3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -68,6 +68,7 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => public bool Debug { get; set; } public bool NeedsInternet { get; set; } public bool EmbedAssemblies { get; set; } + public string? PackageNamingPolicy { get; set; } public string? ManifestPlaceholders { get; set; } public string? CheckedBuild { get; set; } public string? ApplicationJavaClass { get; set; } @@ -131,7 +132,8 @@ public override bool RunTask () frameworkAssemblyNames, useSharedTypemapUniverse: !Debug, manifestConfig, - manifestTemplate); + manifestTemplate, + PackageNamingPolicy); GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in index 02589ee9384..95727a3c42d 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in @@ -21,7 +21,7 @@ {abi}{versionCode:D5} UpdateGeneratedFiles True - LowercaseCrc64 + XxHash64 False @BUNDLETOOL_VERSION@ <_XamarinAndroidMSBuildDirectory>$(MSBuildThisFileDirectory) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 98b28be0591..bf997025616 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -36,6 +36,18 @@ private protected static string TestFixtureAssemblyPath { private protected static List ScanFixtures () => _cachedScanResult.Value.peers; + private protected static List ScanFixtures (string packageNamingPolicy) + { + using var scanner = new JavaPeerScanner (packageNamingPolicy); + var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); + var mdReader = peReader.GetMetadataReader (); + var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); + var assemblies = new [] { (assemblyName, peReader) }; + var peers = scanner.Scan (assemblies); + peReader.Dispose (); + return peers; + } + private protected static AssemblyManifestInfo ScanAssemblyManifestInfo () => _cachedScanResult.Value.manifestInfo; private protected static JavaPeerInfo FindFixtureByJavaName (string javaName) @@ -54,6 +66,14 @@ private protected static JavaPeerInfo FindFixtureByManagedName (string managedNa return peer; } + private protected static JavaPeerInfo FindFixtureByManagedName (string managedName, string packageNamingPolicy) + { + var peers = ScanFixtures (packageNamingPolicy); + var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName); + Assert.NotNull (peer); + return peer; + } + static (string ns, string shortName) ParseManagedTypeName (string managedName) { var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index 81201d21439..3fd457f7346 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; +using Java.Interop.Tools.JavaCallableWrappers; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -95,4 +97,18 @@ public void Scan_UnregisteredType_UsesHashedPackageName (string managedName, str { Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); } + + [Fact] + public void Scan_UnregisteredType_LowercaseCrc64Policy_UsesLegacyCrc64Hash () + { + const string managedName = "MyApp.PlainActivitySubclass"; + var withXxHash64 = FindFixtureByManagedName (managedName).JavaName; + var withCrc64 = FindFixtureByManagedName (managedName, "LowercaseCrc64").JavaName; + + var data = Encoding.UTF8.GetBytes ("MyApp:TestFixtures"); + var expectedHash = string.Concat (Crc64Helper.Compute (data).Select (b => b.ToString ("x2"))); + Assert.Equal ($"crc64{expectedHash}/PlainActivitySubclass", withCrc64); + Assert.StartsWith ("xx64", withXxHash64); + Assert.NotEqual (withXxHash64, withCrc64); + } } From 8d5709b9d5c15c37dcb7a06d7b16c6941ae17923 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:13:27 +0000 Subject: [PATCH 06/10] [TrimmableTypeMap] reduce hashing allocations in JavaPeerScanner Agent-Logs-Url: https://github.com/dotnet/android/sessions/9fd6ed14-5c7b-463e-b1b0-ebefd496f067 Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 102 ++++++++++++++---- 1 file changed, 84 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 6759b46738d..caab3320fde 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; @@ -1485,8 +1486,8 @@ string GetHashedPackageName (string ns, string assemblyName) } return packageNamingPolicy switch { - HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ToLegacyCrc64 (ns + ":" + assemblyName), - _ => "xx64" + ToXxHash64 (ns + ":" + assemblyName), + HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ToLegacyCrc64 (ns, assemblyName), + _ => "xx64" + ToXxHash64 (ns, assemblyName), }; } @@ -1499,29 +1500,94 @@ static HashedPackageNamingPolicy ParsePackageNamingPolicy (string? packageNaming return HashedPackageNamingPolicy.XxHash64; } - static string ToLegacyCrc64 (string value) + static string ToLegacyCrc64 (string ns, string assemblyName) { - var data = System.Text.Encoding.UTF8.GetBytes (value); - var hash = Crc64Helper.Compute (data); - var buf = new char [hash.Length * 2]; - int i = 0; - foreach (var b in hash) { - buf [i++] = GetHexLowerChar (b >> 4); - buf [i++] = GetHexLowerChar (b & 0xF); - } - return new string (buf); + int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); + byte[] rented = ArrayPool.Shared.Rent (byteCount); + try { + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); + ulong crc = ulong.MaxValue; + ulong length = 0; + Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length); + Span hash = stackalloc byte [8]; + WriteUInt64LittleEndian (hash, crc ^ length); + return ToHexString (hash, lowercase: true); + } finally { + ArrayPool.Shared.Return (rented); + } + } + + static string ToXxHash64 (string ns, string assemblyName) + { + int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); + byte[] rented = ArrayPool.Shared.Rent (byteCount); + try { + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); + Span hash = stackalloc byte [8]; + System.IO.Hashing.XxHash64.Hash (rented.AsSpan (0, bytesWritten), hash); + return ToHexString (hash, lowercase: true); + } finally { + ArrayPool.Shared.Return (rented); + } + } + + static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) + { + return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName); + } + + static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span destination) + { + int bytesWritten = 0; + fixed (char* nsPtr = ns) + fixed (byte* destinationPtr = destination) { + bytesWritten += System.Text.Encoding.UTF8.GetBytes (nsPtr, ns.Length, destinationPtr, destination.Length); + } + + destination [bytesWritten++] = (byte) ':'; + + fixed (char* assemblyNamePtr = assemblyName) + fixed (byte* destinationPtr = destination) { + bytesWritten += System.Text.Encoding.UTF8.GetBytes (assemblyNamePtr, assemblyName.Length, destinationPtr + bytesWritten, destination.Length - bytesWritten); + } + + return bytesWritten; + } + + static string ToHexString (ReadOnlySpan hash, bool lowercase) + { + const int maxStackCharLength = 128; + int charLength = hash.Length * 2; + Span chars = charLength <= maxStackCharLength + ? stackalloc char [charLength] + : new char [charLength]; + + for (int i = 0, j = 0; i < hash.Length; i += 1, j += 2) { + byte b = hash [i]; + chars [j] = GetHexValue (b / 16, lowercase); + chars [j + 1] = GetHexValue (b % 16, lowercase); + } + + return ((ReadOnlySpan) chars).ToString (); } - static string ToXxHash64 (string value) + static void WriteUInt64LittleEndian (Span destination, ulong value) { - var data = System.Text.Encoding.UTF8.GetBytes (value); - var hash = System.IO.Hashing.XxHash64.Hash (data); - return BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant (); + destination [0] = (byte) value; + destination [1] = (byte) (value >> 8); + destination [2] = (byte) (value >> 16); + destination [3] = (byte) (value >> 24); + destination [4] = (byte) (value >> 32); + destination [5] = (byte) (value >> 40); + destination [6] = (byte) (value >> 48); + destination [7] = (byte) (value >> 56); } - static char GetHexLowerChar (int value) + static char GetHexValue (int value, bool lowercase) { - return (char) (value < 10 ? ('0' + value) : ('a' + value - 10)); + return (char) (value < 10 + ? value + '0' + : value - 10 + (lowercase ? 'a' : 'A')); } static string ExtractNamespace (string fullName) From 05cebc7c2354556a1cc2e00025e046dc5a8fbd4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:19:50 +0000 Subject: [PATCH 07/10] [TrimmableTypeMap] extract scanner hashing helper and scope defaults Agent-Logs-Url: https://github.com/dotnet/android/sessions/144010de-77de-456e-aee5-4db9e9a1a5cb Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 95 +----------------- .../Scanner/ScannerHashingHelper.cs | 98 +++++++++++++++++++ ...soft.Android.Sdk.TypeMap.Trimmable.targets | 4 +- .../Xamarin.Android.Common.props.in | 3 +- .../Scanner/JavaPeerScannerTests.cs | 6 +- .../Scanner/ScannerHashingHelperTests.cs | 21 ++++ 6 files changed, 127 insertions(+), 100 deletions(-) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index caab3320fde..e1de513ec6b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1,5 +1,4 @@ using System; -using System.Buffers; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; @@ -1486,8 +1485,8 @@ string GetHashedPackageName (string ns, string assemblyName) } return packageNamingPolicy switch { - HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ToLegacyCrc64 (ns, assemblyName), - _ => "xx64" + ToXxHash64 (ns, assemblyName), + HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName), + _ => "xx64" + ScannerHashingHelper.ToXxHash64 (ns, assemblyName), }; } @@ -1500,96 +1499,6 @@ static HashedPackageNamingPolicy ParsePackageNamingPolicy (string? packageNaming return HashedPackageNamingPolicy.XxHash64; } - static string ToLegacyCrc64 (string ns, string assemblyName) - { - int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); - byte[] rented = ArrayPool.Shared.Rent (byteCount); - try { - int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); - ulong crc = ulong.MaxValue; - ulong length = 0; - Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length); - Span hash = stackalloc byte [8]; - WriteUInt64LittleEndian (hash, crc ^ length); - return ToHexString (hash, lowercase: true); - } finally { - ArrayPool.Shared.Return (rented); - } - } - - static string ToXxHash64 (string ns, string assemblyName) - { - int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); - byte[] rented = ArrayPool.Shared.Rent (byteCount); - try { - int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); - Span hash = stackalloc byte [8]; - System.IO.Hashing.XxHash64.Hash (rented.AsSpan (0, bytesWritten), hash); - return ToHexString (hash, lowercase: true); - } finally { - ArrayPool.Shared.Return (rented); - } - } - - static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) - { - return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName); - } - - static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span destination) - { - int bytesWritten = 0; - fixed (char* nsPtr = ns) - fixed (byte* destinationPtr = destination) { - bytesWritten += System.Text.Encoding.UTF8.GetBytes (nsPtr, ns.Length, destinationPtr, destination.Length); - } - - destination [bytesWritten++] = (byte) ':'; - - fixed (char* assemblyNamePtr = assemblyName) - fixed (byte* destinationPtr = destination) { - bytesWritten += System.Text.Encoding.UTF8.GetBytes (assemblyNamePtr, assemblyName.Length, destinationPtr + bytesWritten, destination.Length - bytesWritten); - } - - return bytesWritten; - } - - static string ToHexString (ReadOnlySpan hash, bool lowercase) - { - const int maxStackCharLength = 128; - int charLength = hash.Length * 2; - Span chars = charLength <= maxStackCharLength - ? stackalloc char [charLength] - : new char [charLength]; - - for (int i = 0, j = 0; i < hash.Length; i += 1, j += 2) { - byte b = hash [i]; - chars [j] = GetHexValue (b / 16, lowercase); - chars [j + 1] = GetHexValue (b % 16, lowercase); - } - - return ((ReadOnlySpan) chars).ToString (); - } - - static void WriteUInt64LittleEndian (Span destination, ulong value) - { - destination [0] = (byte) value; - destination [1] = (byte) (value >> 8); - destination [2] = (byte) (value >> 16); - destination [3] = (byte) (value >> 24); - destination [4] = (byte) (value >> 32); - destination [5] = (byte) (value >> 40); - destination [6] = (byte) (value >> 48); - destination [7] = (byte) (value >> 56); - } - - static char GetHexValue (int value, bool lowercase) - { - return (char) (value < 10 - ? value + '0' - : value - 10 + (lowercase ? 'a' : 'A')); - } - static string ExtractNamespace (string fullName) { // Strip nested type suffix (e.g., "My.Namespace.Outer+Inner" → "My.Namespace.Outer") diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs new file mode 100644 index 00000000000..c59b1956589 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -0,0 +1,98 @@ +using System; +using System.Buffers; +using Java.Interop.Tools.JavaCallableWrappers; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +internal static class ScannerHashingHelper +{ + internal static string ToLegacyCrc64 (string ns, string assemblyName) + { + int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); + byte[] rented = ArrayPool.Shared.Rent (byteCount); + try { + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); + ulong crc = ulong.MaxValue; + ulong length = 0; + Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length); + Span hash = stackalloc byte [8]; + WriteUInt64LittleEndian (hash, crc ^ length); + return ToHexString (hash, lowercase: true); + } finally { + ArrayPool.Shared.Return (rented); + } + } + + internal static string ToXxHash64 (string ns, string assemblyName) + { + int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); + byte[] rented = ArrayPool.Shared.Rent (byteCount); + try { + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); + Span hash = stackalloc byte [8]; + System.IO.Hashing.XxHash64.Hash (rented.AsSpan (0, bytesWritten), hash); + return ToHexString (hash, lowercase: true); + } finally { + ArrayPool.Shared.Return (rented); + } + } + + static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) + { + return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName); + } + + static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span destination) + { + int bytesWritten = 0; + fixed (char* nsPtr = ns) + fixed (byte* destinationPtr = destination) { + bytesWritten += System.Text.Encoding.UTF8.GetBytes (nsPtr, ns.Length, destinationPtr, destination.Length); + } + + destination [bytesWritten++] = (byte) ':'; + + fixed (char* assemblyNamePtr = assemblyName) + fixed (byte* destinationPtr = destination) { + bytesWritten += System.Text.Encoding.UTF8.GetBytes (assemblyNamePtr, assemblyName.Length, destinationPtr + bytesWritten, destination.Length - bytesWritten); + } + + return bytesWritten; + } + + static string ToHexString (ReadOnlySpan hash, bool lowercase) + { + const int maxStackCharLength = 128; + int charLength = hash.Length * 2; + Span chars = charLength <= maxStackCharLength + ? stackalloc char [charLength] + : new char [charLength]; + + for (int i = 0, j = 0; i < hash.Length; i += 1, j += 2) { + byte b = hash [i]; + chars [j] = GetHexValue (b / 16, lowercase); + chars [j + 1] = GetHexValue (b % 16, lowercase); + } + + return ((ReadOnlySpan) chars).ToString (); + } + + static void WriteUInt64LittleEndian (Span destination, ulong value) + { + destination [0] = (byte) value; + destination [1] = (byte) (value >> 8); + destination [2] = (byte) (value >> 16); + destination [3] = (byte) (value >> 24); + destination [4] = (byte) (value >> 32); + destination [5] = (byte) (value >> 40); + destination [6] = (byte) (value >> 48); + destination [7] = (byte) (value >> 56); + } + + static char GetHexValue (int value, bool lowercase) + { + return (char) (value < 10 + ? value + '0' + : value - 10 + (lowercase ? 'a' : 'A')); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 9ab79fbc2b3..b8563874aeb 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -11,6 +11,8 @@ <_TypeMapAssemblyName>_Microsoft.Android.TypeMaps + <_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_AndroidPackageNamingPolicySetByUser)' == 'true' ">$(AndroidPackageNamingPolicy) + <_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_TrimmableTypeMapPackageNamingPolicy)' == '' ">XxHash64 true @@ -98,7 +100,7 @@ Debug="$(AndroidIncludeDebugSymbols)" NeedsInternet="$(AndroidNeedsInternetPermission)" EmbedAssemblies="$(EmbedAssembliesIntoApk)" - PackageNamingPolicy="$(AndroidPackageNamingPolicy)" + PackageNamingPolicy="$(_TrimmableTypeMapPackageNamingPolicy)" ManifestPlaceholders="$(AndroidManifestPlaceholders)" CheckedBuild="$(_AndroidCheckedBuild)" ApplicationJavaClass="$(AndroidApplicationJavaClass)" diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in index 95727a3c42d..b13f3abe411 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in @@ -21,7 +21,8 @@ {abi}{versionCode:D5} UpdateGeneratedFiles True - XxHash64 + <_AndroidPackageNamingPolicySetByUser Condition=" '$(AndroidPackageNamingPolicy)' != '' ">true + LowercaseCrc64 False @BUNDLETOOL_VERSION@ <_XamarinAndroidMSBuildDirectory>$(MSBuildThisFileDirectory) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index 3fd457f7346..c27f9b0c515 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using Java.Interop.Tools.JavaCallableWrappers; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -105,9 +103,7 @@ public void Scan_UnregisteredType_LowercaseCrc64Policy_UsesLegacyCrc64Hash () var withXxHash64 = FindFixtureByManagedName (managedName).JavaName; var withCrc64 = FindFixtureByManagedName (managedName, "LowercaseCrc64").JavaName; - var data = Encoding.UTF8.GetBytes ("MyApp:TestFixtures"); - var expectedHash = string.Concat (Crc64Helper.Compute (data).Select (b => b.ToString ("x2"))); - Assert.Equal ($"crc64{expectedHash}/PlainActivitySubclass", withCrc64); + Assert.Equal ("crc64ec59e927bc71f4d8/PlainActivitySubclass", withCrc64); Assert.StartsWith ("xx64", withXxHash64); Assert.NotEqual (withXxHash64, withCrc64); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs new file mode 100644 index 00000000000..a17c6924ea5 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs @@ -0,0 +1,21 @@ +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class ScannerHashingHelperTests +{ + [Theory] + [InlineData ("MyApp", "TestFixtures", "ec59e927bc71f4d8")] + [InlineData ("System.Collections.Generic", "My.Assembly", "9ff866e93b19f500")] + [InlineData ("Hello", "World", "f6bdbfa73a558c54")] + public void ToLegacyCrc64_KnownInputs_HaveStableOutput (string ns, string assemblyName, string expected) + { + Assert.Equal (expected, ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName)); + } + + [Fact] + public void ToXxHash64_KnownInput_HasStableOutput () + { + Assert.Equal ("03e39dfcc696a727", ScannerHashingHelper.ToXxHash64 ("MyApp", "TestFixtures")); + } +} From e9ae72c9ba148539fc7fd3739fd77b42e5cc8319 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:50:07 +0000 Subject: [PATCH 08/10] [TrimmableTypeMap] default trimmable policy to Crc64 and keep LowercaseCrc64 legacy Agent-Logs-Url: https://github.com/dotnet/android/sessions/fdb58e79-42f0-4d7d-8180-2b58c52ae3f8 Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Generator/ManifestGenerator.cs | 2 +- .../Scanner/JavaPeerScanner.cs | 7 ++++- .../Scanner/ScannerHashingHelper.cs | 14 ++++++++++ ...soft.Android.Sdk.TypeMap.Trimmable.targets | 2 +- .../Generator/TypeMapModelBuilderTests.cs | 2 +- .../Scanner/JavaPeerScannerTests.Behavior.cs | 2 +- .../Scanner/JavaPeerScannerTests.EdgeCases.cs | 4 +-- .../Scanner/JavaPeerScannerTests.cs | 28 ++++++++++++------- .../Scanner/ScannerHashingHelperTests.cs | 6 ++++ 9 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index aa0ad3ea57b..553ba08972f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -148,7 +148,7 @@ XDocument CreateDefaultManifest () /// /// Manifest templates may use compat JNI names (e.g., "android.apptests.App") - /// but the trimmable path generates JCWs with hashed package names (e.g., "xx64.../App"). + /// but the trimmable path generates JCWs with hashed package names (e.g., "crc64.../App"). /// This method rewrites any compat name references to the actual JCW name so the /// Android runtime can find the class. /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index e1de513ec6b..5146ba0398e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -18,6 +18,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; public sealed class JavaPeerScanner : IDisposable { enum HashedPackageNamingPolicy { + Crc64, XxHash64, LowercaseCrc64, } @@ -1486,6 +1487,7 @@ string GetHashedPackageName (string ns, string assemblyName) return packageNamingPolicy switch { HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName), + HashedPackageNamingPolicy.Crc64 => "crc64" + ScannerHashingHelper.ToCrc64 (ns, assemblyName), _ => "xx64" + ScannerHashingHelper.ToXxHash64 (ns, assemblyName), }; } @@ -1495,8 +1497,11 @@ static HashedPackageNamingPolicy ParsePackageNamingPolicy (string? packageNaming if (string.Equals (packageNamingPolicy, "LowercaseCrc64", StringComparison.OrdinalIgnoreCase)) { return HashedPackageNamingPolicy.LowercaseCrc64; } + if (string.Equals (packageNamingPolicy, "XxHash64", StringComparison.OrdinalIgnoreCase)) { + return HashedPackageNamingPolicy.XxHash64; + } - return HashedPackageNamingPolicy.XxHash64; + return HashedPackageNamingPolicy.Crc64; } static string ExtractNamespace (string fullName) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs index c59b1956589..f30b5b41bab 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -37,6 +37,20 @@ internal static string ToXxHash64 (string ns, string assemblyName) } } + internal static string ToCrc64 (string ns, string assemblyName) + { + int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); + byte[] rented = ArrayPool.Shared.Rent (byteCount); + try { + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); + Span hash = stackalloc byte [8]; + System.IO.Hashing.Crc64.Hash (rented.AsSpan (0, bytesWritten), hash); + return ToHexString (hash, lowercase: true); + } finally { + ArrayPool.Shared.Return (rented); + } + } + static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) { return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 16d0e00c278..04135852229 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -12,7 +12,7 @@ <_TypeMapAssemblyName>_Microsoft.Android.TypeMaps <_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_AndroidPackageNamingPolicySetByUser)' == 'true' ">$(AndroidPackageNamingPolicy) - <_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_TrimmableTypeMapPackageNamingPolicy)' == '' ">XxHash64 + <_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_TrimmableTypeMapPackageNamingPolicy)' == '' ">Crc64 diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 1efe5f88b5b..b8fc18064d0 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -270,7 +270,7 @@ public void Build_PeerWithInvoker_CreatesProxy () public void Build_HashedRenamedPeer_StoresFinalJavaNameOnProxy (string managedName) { var peer = FindFixtureByManagedName (managedName); - Assert.StartsWith ("xx64", peer.JavaName); + Assert.StartsWith ("crc64", peer.JavaName); Assert.NotEqual (peer.CompatJniName, peer.JavaName); var model = BuildModel (new [] { peer }, "MyTypeMap"); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs index 365cfaf4bef..ec8ba3e7ed0 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -114,7 +114,7 @@ public void Scan_CompatJniName (string javaName, string expectedCompat) public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace () { var unregistered = FindFixtureByManagedName ("MyApp.UnregisteredHelper"); - Assert.StartsWith ("xx64", unregistered.JavaName); + Assert.StartsWith ("crc64", unregistered.JavaName); Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index 2a772cbe98e..6db3290c015 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -22,7 +22,7 @@ public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered () Assert.Equal ("android/app/Activity", baseType.BaseJavaName); var derived = FindFixtureByManagedName ("MyApp.DerivedFromComponentBase"); - Assert.StartsWith ("xx64", derived.JavaName); + Assert.StartsWith ("crc64", derived.JavaName); } [Theory] @@ -56,7 +56,7 @@ public void Scan_EmptyNamespace_Handled () [InlineData ("MyApp.UnregisteredExporter")] public void Scan_UnregisteredType_DiscoveredWithHashedName (string managedName) { - Assert.StartsWith ("xx64", FindFixtureByManagedName (managedName).JavaName); + Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index c27f9b0c515..364784095d3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -88,23 +88,31 @@ public void Scan_RegisterAttribute_DotFormat_NormalizedToSlashes () } [Theory] - [InlineData ("MyApp.PlainActivitySubclass", "xx6403e39dfcc696a727/PlainActivitySubclass")] - [InlineData ("MyApp.UnregisteredClickListener", "xx6403e39dfcc696a727/UnregisteredClickListener")] - [InlineData ("MyApp.UnregisteredExporter", "xx6403e39dfcc696a727/UnregisteredExporter")] + [InlineData ("MyApp.PlainActivitySubclass", "PlainActivitySubclass")] + [InlineData ("MyApp.UnregisteredClickListener", "UnregisteredClickListener")] + [InlineData ("MyApp.UnregisteredExporter", "UnregisteredExporter")] public void Scan_UnregisteredType_UsesHashedPackageName (string managedName, string expectedJavaName) { - Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); + Assert.Equal ($"crc64{ScannerHashingHelper.ToCrc64 ("MyApp", "TestFixtures")}/{expectedJavaName}", + FindFixtureByManagedName (managedName).JavaName); } [Fact] - public void Scan_UnregisteredType_LowercaseCrc64Policy_UsesLegacyCrc64Hash () + public void Scan_UnregisteredType_Crc64Default_DiffersFromLegacyLowercaseCrc64Policy () { const string managedName = "MyApp.PlainActivitySubclass"; - var withXxHash64 = FindFixtureByManagedName (managedName).JavaName; - var withCrc64 = FindFixtureByManagedName (managedName, "LowercaseCrc64").JavaName; + var withCrc64 = FindFixtureByManagedName (managedName).JavaName; + var withLowercaseCrc64 = FindFixtureByManagedName (managedName, "LowercaseCrc64").JavaName; - Assert.Equal ("crc64ec59e927bc71f4d8/PlainActivitySubclass", withCrc64); - Assert.StartsWith ("xx64", withXxHash64); - Assert.NotEqual (withXxHash64, withCrc64); + Assert.Equal ($"crc64{ScannerHashingHelper.ToCrc64 ("MyApp", "TestFixtures")}/PlainActivitySubclass", withCrc64); + Assert.Equal ("crc64ec59e927bc71f4d8/PlainActivitySubclass", withLowercaseCrc64); + Assert.NotEqual (withCrc64, withLowercaseCrc64); + } + + [Fact] + public void Scan_UnregisteredType_XxHash64Policy_UsesXxHash64Hash () + { + var withXxHash64 = FindFixtureByManagedName ("MyApp.PlainActivitySubclass", "XxHash64").JavaName; + Assert.Equal ("xx6403e39dfcc696a727/PlainActivitySubclass", withXxHash64); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs index a17c6924ea5..81bcdc88964 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs @@ -18,4 +18,10 @@ public void ToXxHash64_KnownInput_HasStableOutput () { Assert.Equal ("03e39dfcc696a727", ScannerHashingHelper.ToXxHash64 ("MyApp", "TestFixtures")); } + + [Fact] + public void ToCrc64_KnownInput_HasStableOutput () + { + Assert.Equal ("eb3df85c64aa1af6", ScannerHashingHelper.ToCrc64 ("MyApp", "TestFixtures")); + } } From fa8493df91a76213875e38c90980b421f8e1b66c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:10:04 +0000 Subject: [PATCH 09/10] [TrimmableTypeMap] remove xxhash64 path and improve Crc64 hashing buffers Agent-Logs-Url: https://github.com/dotnet/android/sessions/3701f7a2-7d92-4e63-8d34-f4723682c1ba Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 8 +-- .../Scanner/ScannerHashingHelper.cs | 53 ++++++------------- .../Scanner/JavaPeerScannerTests.cs | 6 --- .../Scanner/ScannerHashingHelperTests.cs | 15 +++--- 4 files changed, 24 insertions(+), 58 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 5146ba0398e..5cc08abf860 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -19,7 +19,6 @@ public sealed class JavaPeerScanner : IDisposable { enum HashedPackageNamingPolicy { Crc64, - XxHash64, LowercaseCrc64, } @@ -1487,8 +1486,7 @@ string GetHashedPackageName (string ns, string assemblyName) return packageNamingPolicy switch { HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName), - HashedPackageNamingPolicy.Crc64 => "crc64" + ScannerHashingHelper.ToCrc64 (ns, assemblyName), - _ => "xx64" + ScannerHashingHelper.ToXxHash64 (ns, assemblyName), + _ => "crc64" + ScannerHashingHelper.ToCrc64 (ns, assemblyName), }; } @@ -1497,10 +1495,6 @@ static HashedPackageNamingPolicy ParsePackageNamingPolicy (string? packageNaming if (string.Equals (packageNamingPolicy, "LowercaseCrc64", StringComparison.OrdinalIgnoreCase)) { return HashedPackageNamingPolicy.LowercaseCrc64; } - if (string.Equals (packageNamingPolicy, "XxHash64", StringComparison.OrdinalIgnoreCase)) { - return HashedPackageNamingPolicy.XxHash64; - } - return HashedPackageNamingPolicy.Crc64; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs index f30b5b41bab..b855d02fc90 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -1,5 +1,4 @@ using System; -using System.Buffers; using Java.Interop.Tools.JavaCallableWrappers; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -9,46 +8,28 @@ internal static class ScannerHashingHelper internal static string ToLegacyCrc64 (string ns, string assemblyName) { int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); - byte[] rented = ArrayPool.Shared.Rent (byteCount); - try { - int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); - ulong crc = ulong.MaxValue; - ulong length = 0; - Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length); - Span hash = stackalloc byte [8]; - WriteUInt64LittleEndian (hash, crc ^ length); - return ToHexString (hash, lowercase: true); - } finally { - ArrayPool.Shared.Return (rented); - } - } - - internal static string ToXxHash64 (string ns, string assemblyName) - { - int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); - byte[] rented = ArrayPool.Shared.Rent (byteCount); - try { - int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); - Span hash = stackalloc byte [8]; - System.IO.Hashing.XxHash64.Hash (rented.AsSpan (0, bytesWritten), hash); - return ToHexString (hash, lowercase: true); - } finally { - ArrayPool.Shared.Return (rented); - } + byte[] utf8Buffer = new byte [byteCount]; + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer); + ulong crc = ulong.MaxValue; + ulong length = 0; + Crc64Helper.HashCore (utf8Buffer, 0, bytesWritten, ref crc, ref length); + Span hash = stackalloc byte [8]; + WriteUInt64LittleEndian (hash, crc ^ length); + return ToHexString (hash, lowercase: true); } internal static string ToCrc64 (string ns, string assemblyName) { + const int stackallocThresholdBytes = 256; int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); - byte[] rented = ArrayPool.Shared.Rent (byteCount); - try { - int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); - Span hash = stackalloc byte [8]; - System.IO.Hashing.Crc64.Hash (rented.AsSpan (0, bytesWritten), hash); - return ToHexString (hash, lowercase: true); - } finally { - ArrayPool.Shared.Return (rented); - } + Span utf8Buffer = byteCount <= stackallocThresholdBytes + ? stackalloc byte [stackallocThresholdBytes] + : new byte [byteCount]; + + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer.Slice (0, byteCount)); + Span hash = stackalloc byte [8]; + System.IO.Hashing.Crc64.Hash (utf8Buffer.Slice (0, bytesWritten), hash); + return ToHexString (hash, lowercase: true); } static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index 364784095d3..8be0d078cf4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -109,10 +109,4 @@ public void Scan_UnregisteredType_Crc64Default_DiffersFromLegacyLowercaseCrc64Po Assert.NotEqual (withCrc64, withLowercaseCrc64); } - [Fact] - public void Scan_UnregisteredType_XxHash64Policy_UsesXxHash64Hash () - { - var withXxHash64 = FindFixtureByManagedName ("MyApp.PlainActivitySubclass", "XxHash64").JavaName; - Assert.Equal ("xx6403e39dfcc696a727/PlainActivitySubclass", withXxHash64); - } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs index 81bcdc88964..d6fdb902b7a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs @@ -13,15 +13,12 @@ public void ToLegacyCrc64_KnownInputs_HaveStableOutput (string ns, string assemb Assert.Equal (expected, ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName)); } - [Fact] - public void ToXxHash64_KnownInput_HasStableOutput () - { - Assert.Equal ("03e39dfcc696a727", ScannerHashingHelper.ToXxHash64 ("MyApp", "TestFixtures")); - } - - [Fact] - public void ToCrc64_KnownInput_HasStableOutput () + [Theory] + [InlineData ("MyApp", "TestFixtures", "eb3df85c64aa1af6")] + [InlineData ("System.Collections.Generic", "My.Assembly", "403b37c9b3a5014d")] + [InlineData ("Hello", "World", "4f2a517f331e7a2c")] + public void ToCrc64_KnownInputs_HaveStableOutput (string ns, string assemblyName, string expected) { - Assert.Equal ("eb3df85c64aa1af6", ScannerHashingHelper.ToCrc64 ("MyApp", "TestFixtures")); + Assert.Equal (expected, ScannerHashingHelper.ToCrc64 (ns, assemblyName)); } } From ab689ec8b36d6d7e361ea1adb1e0fd1b65b36657 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:14:32 +0000 Subject: [PATCH 10/10] [TrimmableTypeMap] clarify Crc64 switch and keep pooled legacy buffering Agent-Logs-Url: https://github.com/dotnet/android/sessions/3701f7a2-7d92-4e63-8d34-f4723682c1ba Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 3 ++- .../Scanner/ScannerHashingHelper.cs | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 5cc08abf860..0f2e782a200 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1486,7 +1486,8 @@ string GetHashedPackageName (string ns, string assemblyName) return packageNamingPolicy switch { HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName), - _ => "crc64" + ScannerHashingHelper.ToCrc64 (ns, assemblyName), + HashedPackageNamingPolicy.Crc64 => "crc64" + ScannerHashingHelper.ToCrc64 (ns, assemblyName), + _ => throw new InvalidOperationException ($"Unsupported package naming policy: {packageNamingPolicy}"), }; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs index b855d02fc90..b57c78bbb82 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using Java.Interop.Tools.JavaCallableWrappers; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -8,14 +9,18 @@ internal static class ScannerHashingHelper internal static string ToLegacyCrc64 (string ns, string assemblyName) { int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName); - byte[] utf8Buffer = new byte [byteCount]; - int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer); - ulong crc = ulong.MaxValue; - ulong length = 0; - Crc64Helper.HashCore (utf8Buffer, 0, bytesWritten, ref crc, ref length); - Span hash = stackalloc byte [8]; - WriteUInt64LittleEndian (hash, crc ^ length); - return ToHexString (hash, lowercase: true); + byte[] rented = ArrayPool.Shared.Rent (byteCount); + try { + int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount)); + ulong crc = ulong.MaxValue; + ulong length = 0; + Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length); + Span hash = stackalloc byte [8]; + WriteUInt64LittleEndian (hash, crc ^ length); + return ToHexString (hash, lowercase: true); + } finally { + ArrayPool.Shared.Return (rented); + } } internal static string ToCrc64 (string ns, string assemblyName)