From 18d6f0faa54f0740f62f3331a976238d5cc471e0 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 13:54:02 +0200 Subject: [PATCH 1/9] Compare trimmable typemap APK contents Add a Release CoreCLR HelloWorld comparison test that builds llvm-ir and trimmable typemap APKs, prints managed and dex diagnostics, and asserts the trimmable typemap does not retain extra typemap-eligible managed or Java entries. Pass all generated typemap assemblies to ILLink as typemap entry assemblies and mark them trimmable so conditional typemap entries are honored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 9 +- .../TrimmableTypeMapBuildTests.cs | 727 +++++++++++++++++- 2 files changed, 733 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 706a73ae31e..c7b12d09705 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -12,6 +12,7 @@ %(Filename)%(Extension) + true @@ -20,9 +21,15 @@ + + <_TrimmableTypeMapEntryAssemblies Include="$(_TypeMapOutputDirectory)*.dll" /> + - <_ExtraTrimmerArgs>--typemap-entry-assembly $(_TypeMapAssemblyName) $(_ExtraTrimmerArgs) + <_ExtraTrimmerArgs>@(_TrimmableTypeMapEntryAssemblies->'--typemap-entry-assembly %(Filename)', ' ') $(_ExtraTrimmerArgs) + + <_TrimmableTypeMapEntryAssemblies Remove="@(_TrimmableTypeMapEntryAssemblies)" /> + + + + <_LinkedAssemblyForProguard Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> + + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 10d9cf9f413..4bde3d49fc9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -537,6 +537,7 @@ ApkComparisonProfile BuildTypemapComparisonApk (string typemapImplementation) proj.SetRuntime (AndroidRuntime.CoreCLR); proj.SetProperty ("AndroidSupportedAbis", "arm64-v8a"); proj.SetProperty ("AndroidPackageFormat", "apk"); + proj.SetProperty (KnownProperties.AndroidLinkTool, "r8"); proj.SetProperty ("TrimMode", "full"); proj.SetProperty ("_AndroidTypeMapImplementation", typemapImplementation); From 91d61f763594dec98cb372e5545bfa480e8d5f83 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 14:08:13 +0200 Subject: [PATCH 3/9] Pass per-assembly typemap entries to ILLink Limit --typemap-entry-assembly arguments to generated per-assembly *.TypeMap.dll assemblies that contain TypeMapAttribute entries, instead of treating the root typemap loader assembly as an entry assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 96880001878..6efe05d59b4 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -26,7 +26,7 @@ BeforeTargets="PrepareForILLink;_RunILLink" DependsOnTargets="_GenerateTrimmableTypeMap"> - <_TrimmableTypeMapEntryAssemblies Include="$(_TypeMapOutputDirectory)*.dll" /> + <_TrimmableTypeMapEntryAssemblies Include="$(_TypeMapOutputDirectory)*.TypeMap.dll" /> <_ExtraTrimmerArgs>@(_TrimmableTypeMapEntryAssemblies->'--typemap-entry-assembly %(Filename)', ' ') $(_ExtraTrimmerArgs) From 657165ff018dba23daf2884a1468814c3c6d2c71 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 16:23:53 +0200 Subject: [PATCH 4/9] Refine trimmable typemap framework roots Keep SDK framework ACWs conditional unless they are explicitly rooted, and pass framework assembly names through trimmable typemap generation. This allows Mono.Android implementor entries to be trimmed while preserving app ACWs and scanner-rooted components. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerInfo.cs | 6 +- .../TrimmableTypeMapGenerator.cs | 10 ++ ...soft.Android.Sdk.TypeMap.Trimmable.targets | 1 + .../Tasks/GenerateTrimmableTypeMap.cs | 12 +- .../TrimmableTypeMapBuildTests.cs | 122 +++++++++++++++++- .../Generator/TypeMapModelBuilderTests.cs | 26 ++++ 6 files changed, 171 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 85fe427feb2..7382a4ed60a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -44,9 +44,11 @@ public sealed record JavaPeerInfo public required string AssemblyName { get; init; } /// - /// True when the type belongs to a framework assembly. + /// True when the type belongs to a framework assembly supplied by the Android SDK. + /// Framework ACWs are generated by the SDK and can be trimmed like bindings unless + /// another rule explicitly roots them. /// - public bool IsFrameworkAssembly { get; init; } + public bool IsFrameworkAssembly { get; set; } /// /// True when per-rank array typemap entries should be generated for this peer. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 5a391f520b6..652c4fb3a5b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -48,6 +48,7 @@ public TrimmableTypeMapResult Execute ( logger.LogNoJavaPeerTypesFound (); return new TrimmableTypeMapResult ([], [], allPeers); } + MarkFrameworkAssemblyPeers (allPeers, frameworkAssemblyNames); RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); PropagateDeferredRegistrationToBaseClasses (allPeers); @@ -212,6 +213,15 @@ List GenerateTypeMapAssemblies ( return generatedAssemblies; } + static void MarkFrameworkAssemblyPeers (List allPeers, HashSet frameworkAssemblyNames) + { + foreach (var peer in allPeers) { + if (frameworkAssemblyNames.Contains (peer.AssemblyName)) { + peer.IsFrameworkAssembly = true; + } + } + } + /// /// Groups peers by assembly, merging cross-assembly aliases into a single group. /// When the same JNI name appears in multiple assemblies (e.g. Java.Lang.Object 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 0490b602d11..80403abf96b 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 @@ -89,6 +89,7 @@ @@ -46,6 +52,7 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN [Required] public ITaskItem [] ResolvedAssemblies { get; set; } = []; public ITaskItem [] ResolvedFrameworkAssemblies { get; set; } = []; + public string [] FrameworkAssemblyNames { get; set; } = []; [Required] public string OutputDirectory { get; set; } = ""; [Required] @@ -105,7 +112,10 @@ public override bool RunTask () Path: g.Key, IsFrameworkAssembly: frameworkAssemblyPaths.Contains (g.Key) || g.Any (IsFrameworkAssemblyItem))) .ToList (); - var frameworkAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); + var frameworkAssemblyNames = new HashSet (DefaultFrameworkAssemblyNames, StringComparer.OrdinalIgnoreCase); + foreach (var assemblyName in FrameworkAssemblyNames) { + frameworkAssemblyNames.Add (assemblyName); + } Directory.CreateDirectory (OutputDirectory); Directory.CreateDirectory (JavaSourceOutputDirectory); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 4bde3d49fc9..343cd66942a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -553,8 +553,10 @@ ApkComparisonProfile BuildTypemapComparisonApk (string typemapImplementation) var apkPath = Directory.GetFiles (apkDirectory, "*-Signed.apk", SearchOption.AllDirectories).Single (); var acwMapPath = builder.Output.GetIntermediaryPath ("acw-map.txt"); var javaSourceDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android", "src")); + var typeMapDirectory = builder.Output.GetIntermediaryPath ("typemap"); + var linkedAssemblyDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "linked")); - var profile = ReadApkProfile (typemapImplementation, apkPath, acwMapPath, javaSourceDirectory); + var profile = ReadApkProfile (typemapImplementation, apkPath, acwMapPath, javaSourceDirectory, typeMapDirectory, linkedAssemblyDirectory); if (typemapImplementation == "trimmable") { Assert.IsTrue (profile.ManagedAssemblyNames.Contains ("_Microsoft.Android.TypeMaps.dll"), "trimmable build should package the root managed typemap assembly."); } else { @@ -563,7 +565,7 @@ ApkComparisonProfile BuildTypemapComparisonApk (string typemapImplementation) return profile; } - ApkComparisonProfile ReadApkProfile (string name, string apkPath, string acwMapPath, string javaSourceDirectory) + ApkComparisonProfile ReadApkProfile (string name, string apkPath, string acwMapPath, string javaSourceDirectory, string typeMapDirectory, string linkedAssemblyDirectory) { var profile = new ApkComparisonProfile { Name = name, @@ -573,6 +575,8 @@ ApkComparisonProfile ReadApkProfile (string name, string apkPath, string acwMapP LoadAcwMap (acwMapPath, profile); ReadGeneratedJavaProfile (javaSourceDirectory, profile); + ReadTypeMapAssemblyProfile (profile, "generated", typeMapDirectory); + ReadTypeMapAssemblyProfile (profile, "linked", linkedAssemblyDirectory); ReadAssemblyStoreProfile (profile); ReadDexProfile (profile); @@ -674,6 +678,9 @@ void ReadAssemblyStoreProfile (ApkComparisonProfile profile) } using (assembly) { profile.ManagedAssemblyCount++; + if (item.Name.EndsWith (".TypeMap.dll", StringComparison.Ordinal) || item.Name == "_Microsoft.Android.TypeMaps.dll") { + ReadTypeMapAssemblyProfile (profile, "packaged", assembly, item.Name); + } foreach (var type in assembly.Modules.SelectMany (m => m.Types).SelectMany (FlattenType)) { if (IsTypemapHelperManagedType (type.FullName)) { continue; @@ -697,6 +704,66 @@ void ReadAssemblyStoreProfile (ApkComparisonProfile profile) } } + void ReadTypeMapAssemblyProfile (ApkComparisonProfile profile, string stage, string directory) + { + if (!Directory.Exists (directory)) { + return; + } + + foreach (var file in Directory.EnumerateFiles (directory, "*.dll", SearchOption.TopDirectoryOnly).Where (IsTypeMapAssemblyPath)) { + using var assembly = AssemblyDefinition.ReadAssembly (file); + ReadTypeMapAssemblyProfile (profile, stage, assembly, Path.GetFileName (file)); + } + } + + bool IsTypeMapAssemblyPath (string file) + { + var name = Path.GetFileName (file); + return name.EndsWith (".TypeMap.dll", StringComparison.Ordinal) || name == "_Microsoft.Android.TypeMaps.dll"; + } + + void ReadTypeMapAssemblyProfile (ApkComparisonProfile profile, string stage, AssemblyDefinition assembly, string assemblyName) + { + var metrics = new TypeMapAssemblyMetrics { + Stage = stage, + AssemblyName = assemblyName, + }; + + foreach (var attribute in assembly.CustomAttributes) { + var attributeName = attribute.AttributeType.FullName; + if (attributeName.StartsWith ("System.Runtime.InteropServices.TypeMapAttribute`1", StringComparison.Ordinal)) { + ReadTypeMapAttribute (attribute, metrics); + } else if (attributeName.StartsWith ("System.Runtime.InteropServices.TypeMapAssociationAttribute", StringComparison.Ordinal)) { + metrics.AssociationAttributeCount++; + } else if (attributeName.StartsWith ("System.Runtime.InteropServices.TypeMapAssemblyTargetAttribute`1", StringComparison.Ordinal)) { + metrics.AssemblyTargetAttributeCount++; + } + } + + if (metrics.TypeMapAttributeCount != 0 || metrics.AssociationAttributeCount != 0 || metrics.AssemblyTargetAttributeCount != 0) { + profile.TypeMapAssemblies.Add (metrics); + } + } + + void ReadTypeMapAttribute (CustomAttribute attribute, TypeMapAssemblyMetrics metrics) + { + metrics.TypeMapAttributeCount++; + if (attribute.ConstructorArguments.Count == 2) { + metrics.UnconditionalTypeMapAttributeCount++; + } else if (attribute.ConstructorArguments.Count == 3) { + metrics.ConditionalTypeMapAttributeCount++; + } + + var jniName = attribute.ConstructorArguments.Count > 0 ? attribute.ConstructorArguments [0].Value as string : null; + var proxyType = attribute.ConstructorArguments.Count > 1 ? attribute.ConstructorArguments [1].Value as string : null; + var targetType = attribute.ConstructorArguments.Count > 2 ? attribute.ConstructorArguments [2].Value as string : null; + var key = $"{jniName}\t{proxyType}\t{targetType}"; + metrics.TypeMapAttributeKeys.Add (key); + if (jniName != null) { + metrics.IncrementPrefixBucket (jniName); + } + } + bool IsManagedTypemapEligible (TypeDefinition type, ApkComparisonProfile profile) { if (profile.CandidateManagedTypes.Contains (type.FullName)) { @@ -892,7 +959,7 @@ void WriteComparisonTable (ApkComparisonProfile llvmIr, ApkComparisonProfile tri TestContext.Out.WriteLine ($"| APK size | {FormatNumber (llvmIr.ApkSize)} | {FormatNumber (trimmable.ApkSize)} |"); TestContext.Out.WriteLine ($"| Assembly-store payload | {FormatNumber (llvmIr.AssemblyStoreSize)} | {FormatNumber (trimmable.AssemblyStoreSize)} |"); TestContext.Out.WriteLine ($"| classes*.dex | {FormatNumber (llvmIr.DexSize)} | {FormatNumber (trimmable.DexSize)} |"); - TestContext.Out.WriteLine ($"| Filtered managed types / methods | {FormatNumber (llvmIr.FilteredManagedTypeCount)} / {FormatNumber (llvmIr.FilteredManagedMethodCount)} | {FormatNumber (trimmable.FilteredManagedTypeCount)} / {FormatNumber (trimmable.FilteredManagedMethodCount)} |"); + TestContext.Out.WriteLine ($"| Registered managed types / methods | {FormatNumber (llvmIr.FilteredManagedTypeCount)} / {FormatNumber (llvmIr.FilteredManagedMethodCount)} | {FormatNumber (trimmable.FilteredManagedTypeCount)} / {FormatNumber (trimmable.FilteredManagedMethodCount)} |"); TestContext.Out.WriteLine ($"| Managed diff | {FormatNumber (managedDiff.LlvmIrOnly.Length)} llvm-ir-only | {FormatNumber (managedDiff.TrimmableOnly.Length)} trimmable-only |"); TestContext.Out.WriteLine ($"| Java diff | {FormatNumber (javaDiff.LlvmIrOnly.Length)} llvm-ir-only | {FormatNumber (javaDiff.TrimmableOnly.Length)} trimmable-only |"); } @@ -908,6 +975,9 @@ void WriteProfile (ApkComparisonProfile profile) TestContext.Out.WriteLine ($"{profile.Name}: generated Java sources={profile.GeneratedJavaSourceCount}, __md_methods files={profile.GeneratedJavaWithMdMethodsCount}, Runtime.register files={profile.GeneratedJavaWithRuntimeRegisterCount}, Runtime.registerNatives files={profile.GeneratedJavaWithRegisterNativesCount}"); TestContext.Out.WriteLine ($"{profile.Name}: assembly stores: {String.Join ("; ", profile.AssemblyStores)}"); TestContext.Out.WriteLine ($"{profile.Name}: dex files: {String.Join ("; ", profile.DexFiles)}"); + foreach (var metrics in profile.TypeMapAssemblies) { + TestContext.Out.WriteLine ($"{profile.Name}: typemap {metrics.Stage}/{metrics.AssemblyName}: typemap={metrics.TypeMapAttributeCount}, unique={metrics.UniqueTypeMapAttributeCount}, duplicates={metrics.DuplicateTypeMapAttributeCount}, unconditional={metrics.UnconditionalTypeMapAttributeCount}, conditional={metrics.ConditionalTypeMapAttributeCount}, associations={metrics.AssociationAttributeCount}, assembly-targets={metrics.AssemblyTargetAttributeCount}, prefixes={metrics.FormatPrefixBuckets ()}"); + } } void WriteSize (string label, long llvmIr, long trimmable) @@ -982,6 +1052,52 @@ class ApkComparisonProfile public readonly HashSet ManagedTypemapEntries = new HashSet (StringComparer.Ordinal); public readonly HashSet JavaTypemapEntries = new HashSet (StringComparer.Ordinal); public readonly HashSet JavaClassNames = new HashSet (StringComparer.Ordinal); + public readonly List TypeMapAssemblies = new List (); + } + + class TypeMapAssemblyMetrics + { + public string Stage; + public string AssemblyName; + public int TypeMapAttributeCount; + public int UnconditionalTypeMapAttributeCount; + public int ConditionalTypeMapAttributeCount; + public int AssociationAttributeCount; + public int AssemblyTargetAttributeCount; + public readonly List TypeMapAttributeKeys = new List (); + readonly SortedDictionary prefixBuckets = new SortedDictionary (StringComparer.Ordinal); + + public int UniqueTypeMapAttributeCount => TypeMapAttributeKeys.Distinct (StringComparer.Ordinal).Count (); + public int DuplicateTypeMapAttributeCount => TypeMapAttributeCount - UniqueTypeMapAttributeCount; + + public void IncrementPrefixBucket (string jniName) + { + var bucket = GetPrefixBucket (jniName); + prefixBuckets.TryGetValue (bucket, out int count); + prefixBuckets [bucket] = count + 1; + } + + public string FormatPrefixBuckets () + { + return String.Join (", ", prefixBuckets.Select (p => $"{p.Key}={p.Value}")); + } + + static string GetPrefixBucket (string jniName) + { + if (jniName.StartsWith ("mono/android/", StringComparison.Ordinal)) { + return "mono/android"; + } + if (jniName.StartsWith ("android/", StringComparison.Ordinal)) { + return "android"; + } + if (jniName.StartsWith ("java/", StringComparison.Ordinal)) { + return "java"; + } + if (jniName.StartsWith ("com/xamarin/", StringComparison.Ordinal)) { + return "app"; + } + return "other"; + } } class EntryDiff diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 5355347ded5..0a19aa073ac 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -216,6 +216,32 @@ public void Build_UserAcwType_IsUnconditional () Assert.Null (mainEntry.TargetTypeReference); } + [Fact] + public void Build_FrameworkAcwType_IsTrimmable () + { + var peer = MakeAcwPeer ("mono/android/view/View_OnClickListenerImplementor", "Android.Views.View+IOnClickListenerImplementor", "Mono.Android") with { + IsFrameworkAssembly = true, + }; + var model = BuildModel (new [] { peer }); + + var entry = model.Entries.First (e => e.JniName == "mono/android/view/View_OnClickListenerImplementor"); + Assert.False (entry.IsUnconditional); + Assert.Equal ("Android.Views.View+IOnClickListenerImplementor, Mono.Android", entry.TargetTypeReference); + } + + [Fact] + public void Build_FrameworkAcwType_MarkedUnconditional_IsUnconditional () + { + var peer = MakeAcwPeer ("mono/android/app/ApplicationRegistration", "Android.App.ApplicationRegistration", "Mono.Android") with { + IsFrameworkAssembly = true, + IsUnconditional = true, + }; + var model = BuildModel (new [] { peer }); + + Assert.True (model.Entries [0].IsUnconditional); + Assert.Null (model.Entries [0].TargetTypeReference); + } + [Fact] public void Build_McwBinding_IsTrimmable () { From 9ec7a220171709540c93ae8a920f5858f67c533e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 20 May 2026 17:26:07 +0200 Subject: [PATCH 5/9] Support DynamicCodeSupport=false with trimmable typemaps Emit array typemap sentinels when dynamic code support is explicitly disabled so CoreCLR trimmable typemap builds can use the no-dynamic-code array path. Keep DynamicCodeSupport enabled by default and cover the explicit opt-out path in tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 6 +- .../TrimmableTypeMapBuildTests.cs | 79 ++++++++++++++++++- 2 files changed, 81 insertions(+), 4 deletions(-) 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 80403abf96b..aab2ec4f46c 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 @@ -24,10 +24,10 @@ <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java <_TypeMapAssembliesListFile>$(_TypeMapOutputDirectory)typemap-assemblies.txt - - <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' and '$(PublishAot)' == 'true' ">3 + <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' and ('$(PublishAot)' == 'true' or '$(DynamicCodeSupport)' == 'false') ">3 <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' ">0 _RecordTrimmableTypeMapFileWrites; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 343cd66942a..93a6dc6e2b4 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -266,6 +266,24 @@ public void CoreClrTrimmableTypeMap_PackagesReadyToRunTypeMap () } } + [Test] + public void ReleaseCoreClrTrimmableTypeMap_SupportsExplicitDynamicCodeSupportOff () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { + return; + } + + var dynamicCodeDisabledTrimmable = BuildDynamicCodeSupportProfile ("trimmable", dynamicCodeSupport: false); + + const string dynamicCodeSupportFalse = "\"System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported\": false"; + Assert.IsTrue ( + dynamicCodeDisabledTrimmable.RuntimeConfig.Contains (dynamicCodeSupportFalse, StringComparison.Ordinal), + "trimmable typemap builds should honor explicit DynamicCodeSupport=false."); + Assert.IsTrue ( + dynamicCodeDisabledTrimmable.LinkedTypeMapAssembliesContainArrayRankSentinels, + "trimmable typemap builds should emit array typemap sentinels when dynamic code is disabled."); + } + [Test] public void ReleaseCoreClrTrimmableTypeMap_DoesNotKeepMoreTypemapPeersThanLlvmIr () { @@ -318,7 +336,6 @@ public void TrimmableTypeMap_RuntimeArtifacts_ArePackagedInSdk () FileAssert.Exists (Path.Combine (toolsDir, file), $"{file} should exist in the SDK pack."); } } - } // T1: end-to-end build coverage for [Export] and [ExportField] under trimmable. // The trimmable typemap path emits a per-assembly typemap DLL and JCW Java @@ -565,6 +582,60 @@ ApkComparisonProfile BuildTypemapComparisonApk (string typemapImplementation) return profile; } + DynamicCodeSupportProfile BuildDynamicCodeSupportProfile (string typemapImplementation, bool? dynamicCodeSupport) + { + var dynamicCodeSuffix = dynamicCodeSupport.HasValue ? $"_{dynamicCodeSupport.Value.ToString ().ToLowerInvariant ()}" : ""; + var projectName = $"DynamicCodeSupport_{typemapImplementation.Replace ("-", "_")}{dynamicCodeSuffix}"; + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + PackageName = "com.xamarin.dynamiccodesupport", + ProjectName = projectName, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty (KnownProperties.RuntimeIdentifier, "android-arm64"); + proj.SetProperty ("AndroidPackageFormat", "apk"); + proj.SetProperty (KnownProperties.AndroidLinkTool, "r8"); + proj.SetProperty ("TrimMode", "full"); + proj.SetProperty ("PublishReadyToRun", "false"); + proj.SetProperty ("_AndroidTypeMapImplementation", typemapImplementation); + if (dynamicCodeSupport.HasValue) { + proj.SetProperty ("DynamicCodeSupport", dynamicCodeSupport.Value.ToString ().ToLowerInvariant ()); + } + + using var builder = CreateApkBuilder (Path.Combine ("temp", $"{projectName}_{Guid.NewGuid ():N}")); + Assert.IsTrue (builder.Build (proj), $"{typemapImplementation} build should have succeeded."); + + var runtimeConfigPath = FindOutputFile (builder, proj, $"{proj.ProjectName}.runtimeconfig.json"); + var linkedAssemblyDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "linked")); + return new DynamicCodeSupportProfile { + RuntimeConfig = File.ReadAllText (runtimeConfigPath), + LinkedTypeMapAssembliesContainArrayRankSentinels = TypeMapAssembliesContainType (linkedAssemblyDirectory, "__ArrayMapRank1"), + }; + } + + string FindOutputFile (ProjectBuilder builder, XamarinAndroidApplicationProject proj, string fileName) + { + var outputDirectory = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath); + var files = Directory.GetFiles (outputDirectory, fileName, SearchOption.AllDirectories); + Assert.AreEqual (1, files.Length, $"{outputDirectory} should contain one {fileName}."); + return files [0]; + } + + bool TypeMapAssembliesContainType (string directory, string typeName) + { + if (!Directory.Exists (directory)) { + return false; + } + + foreach (var file in Directory.EnumerateFiles (directory, "*.dll", SearchOption.TopDirectoryOnly).Where (IsTypeMapAssemblyPath)) { + using var assembly = AssemblyDefinition.ReadAssembly (file); + if (assembly.Modules.SelectMany (m => m.Types).Any (type => type.Name == typeName)) { + return true; + } + } + + return false; + } ApkComparisonProfile ReadApkProfile (string name, string apkPath, string acwMapPath, string javaSourceDirectory, string typeMapDirectory, string linkedAssemblyDirectory) { var profile = new ApkComparisonProfile { @@ -1055,6 +1126,12 @@ class ApkComparisonProfile public readonly List TypeMapAssemblies = new List (); } + class DynamicCodeSupportProfile + { + public string RuntimeConfig; + public bool LinkedTypeMapAssembliesContainArrayRankSentinels; + } + class TypeMapAssemblyMetrics { public string Stage; From 253a264d6911132424c32c200965e69c5d018de4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 26 May 2026 07:00:29 +0200 Subject: [PATCH 6/9] Keep trimming PR focused on product improvements Remove the APK comparison test scaffolding from this split branch while keeping the DynamicCodeSupport coverage for trimmable typemaps. --- .../TrimmableTypeMapBuildTests.cs | 840 +----------------- 1 file changed, 4 insertions(+), 836 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 93a6dc6e2b4..3d24e6a1ef0 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -1,15 +1,10 @@ using System; using System.Collections.Generic; using System.IO; -using System.IO.Compression; -using System.Globalization; using System.Linq; -using System.Text; using System.Text.RegularExpressions; -using K4os.Compression.LZ4; using Mono.Cecil; using NUnit.Framework; -using Xamarin.Android.AssemblyStore; using Xamarin.Android.Tasks; using Xamarin.Android.Tools; using Xamarin.ProjectTools; @@ -18,7 +13,6 @@ namespace Xamarin.Android.Build.Tests { [TestFixture] [Category ("Node-2")] public class TrimmableTypeMapBuildTests : BaseTest { - const uint CompressedAssemblyMagic = 0x5A4C4158; // 'XALZ', little-endian [Test] public void Build_WithTrimmableTypeMap_Succeeds ([Values] bool isRelease, [Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) @@ -284,26 +278,6 @@ public void ReleaseCoreClrTrimmableTypeMap_SupportsExplicitDynamicCodeSupportOff "trimmable typemap builds should emit array typemap sentinels when dynamic code is disabled."); } - [Test] - public void ReleaseCoreClrTrimmableTypeMap_DoesNotKeepMoreTypemapPeersThanLlvmIr () - { - if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { - return; - } - - var llvmIr = BuildTypemapComparisonApk ("llvm-ir"); - var trimmable = BuildTypemapComparisonApk ("trimmable"); - - WriteComparisonDiagnostics (llvmIr, trimmable); - - Assert.IsEmpty ( - trimmable.ManagedTypemapEntries.Except (llvmIr.ManagedTypemapEntries, StringComparer.Ordinal).OrderBy (x => x, StringComparer.Ordinal).ToArray (), - "Trimmable typemap should not keep additional managed typemap-eligible types or methods compared to llvm-ir."); - Assert.IsEmpty ( - trimmable.JavaTypemapEntries.Except (llvmIr.JavaTypemapEntries, StringComparer.Ordinal).OrderBy (x => x, StringComparer.Ordinal).ToArray (), - "Trimmable typemap should not keep additional Java typemap-eligible classes or methods compared to llvm-ir."); - } - [Test] public void TrimmableTypeMap_PreserveLists_ArePackagedInSdk () { @@ -500,6 +474,7 @@ class ExportShapes : Java.Lang.Object { "assembly or the user's [Export] source. Offending warning lines:\n " + string.Join ("\n ", offending)); } + [Test] public void Build_WithTrimmableTypeMap_AbstractTypeWithProtectedCtor_Succeeds () { @@ -543,45 +518,6 @@ static void AssertTrimmableTypeMapOutputs (string typemapDir) var javaFiles = Directory.GetFiles (javaDir, "*.java", SearchOption.AllDirectories); Assert.IsNotEmpty (javaFiles, "At least one trimmable JCW Java source file should be generated."); } - - ApkComparisonProfile BuildTypemapComparisonApk (string typemapImplementation) - { - var proj = new XamarinAndroidApplicationProject { - IsRelease = true, - PackageName = "com.xamarin.typemapcomparison", - ProjectName = "TypemapComparison", - }; - proj.SetRuntime (AndroidRuntime.CoreCLR); - proj.SetProperty ("AndroidSupportedAbis", "arm64-v8a"); - proj.SetProperty ("AndroidPackageFormat", "apk"); - proj.SetProperty (KnownProperties.AndroidLinkTool, "r8"); - proj.SetProperty ("TrimMode", "full"); - proj.SetProperty ("_AndroidTypeMapImplementation", typemapImplementation); - - using var builder = CreateApkBuilder (Path.Combine ("temp", $"TypemapComparison_{typemapImplementation}_{Guid.NewGuid ():N}")); - Assert.IsTrue (builder.Build (proj), $"{typemapImplementation} build should have succeeded."); - - if (typemapImplementation != "trimmable") { - FileAssert.Exists (builder.Output.GetIntermediaryPath (Path.Combine ("android", "typemaps.arm64-v8a.ll")), "llvm-ir build should generate the native typemap."); - FileAssert.Exists (builder.Output.GetIntermediaryPath (Path.Combine ("android", "typemaps.arm64-v8a.o")), "llvm-ir build should compile the native typemap."); - } - - var apkDirectory = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath); - var apkPath = Directory.GetFiles (apkDirectory, "*-Signed.apk", SearchOption.AllDirectories).Single (); - var acwMapPath = builder.Output.GetIntermediaryPath ("acw-map.txt"); - var javaSourceDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android", "src")); - var typeMapDirectory = builder.Output.GetIntermediaryPath ("typemap"); - var linkedAssemblyDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "linked")); - - var profile = ReadApkProfile (typemapImplementation, apkPath, acwMapPath, javaSourceDirectory, typeMapDirectory, linkedAssemblyDirectory); - if (typemapImplementation == "trimmable") { - Assert.IsTrue (profile.ManagedAssemblyNames.Contains ("_Microsoft.Android.TypeMaps.dll"), "trimmable build should package the root managed typemap assembly."); - } else { - Assert.IsFalse (profile.ManagedAssemblyNames.Contains ("_Microsoft.Android.TypeMaps.dll"), "llvm-ir build should not package the trimmable root managed typemap assembly."); - } - return profile; - } - DynamicCodeSupportProfile BuildDynamicCodeSupportProfile (string typemapImplementation, bool? dynamicCodeSupport) { var dynamicCodeSuffix = dynamicCodeSupport.HasValue ? $"_{dynamicCodeSupport.Value.ToString ().ToLowerInvariant ()}" : ""; @@ -636,494 +572,12 @@ bool TypeMapAssembliesContainType (string directory, string typeName) return false; } - ApkComparisonProfile ReadApkProfile (string name, string apkPath, string acwMapPath, string javaSourceDirectory, string typeMapDirectory, string linkedAssemblyDirectory) - { - var profile = new ApkComparisonProfile { - Name = name, - ApkPath = apkPath, - ApkSize = new FileInfo (apkPath).Length, - }; - - LoadAcwMap (acwMapPath, profile); - ReadGeneratedJavaProfile (javaSourceDirectory, profile); - ReadTypeMapAssemblyProfile (profile, "generated", typeMapDirectory); - ReadTypeMapAssemblyProfile (profile, "linked", linkedAssemblyDirectory); - ReadAssemblyStoreProfile (profile); - ReadDexProfile (profile); - - return profile; - } - - void ReadGeneratedJavaProfile (string javaSourceDirectory, ApkComparisonProfile profile) - { - if (!Directory.Exists (javaSourceDirectory)) { - return; - } - - foreach (var file in Directory.EnumerateFiles (javaSourceDirectory, "*.java", SearchOption.AllDirectories)) { - profile.GeneratedJavaSourceCount++; - var text = File.ReadAllText (file); - if (text.IndexOf ("__md_methods", StringComparison.Ordinal) >= 0) { - profile.GeneratedJavaWithMdMethodsCount++; - } - if (text.IndexOf ("Runtime.register (", StringComparison.Ordinal) >= 0) { - profile.GeneratedJavaWithRuntimeRegisterCount++; - } - if (text.IndexOf ("Runtime.registerNatives", StringComparison.Ordinal) >= 0) { - profile.GeneratedJavaWithRegisterNativesCount++; - } - } - } - - void LoadAcwMap (string acwMapPath, ApkComparisonProfile profile) - { - FileAssert.Exists (acwMapPath, $"{profile.Name} build should produce acw-map.txt."); - - foreach (var line in File.ReadLines (acwMapPath)) { - if (line.Length == 0 || line [0] == '#') { - continue; - } - - var fields = line.Split (new [] { ';' }, 2); - if (fields.Length != 2) { - continue; - } - - var javaName = fields [1].Trim (); - if (!IsTypemapHelperJavaType (javaName)) { - profile.CandidateJavaNames.Add (javaName); - } - - var managedType = GetManagedTypeFromAcwMapKey (fields [0]); - if (managedType != null && !IsTypemapHelperManagedType (managedType)) { - profile.CandidateManagedTypes.Add (managedType); - } - } - } - - string GetManagedTypeFromAcwMapKey (string key) - { - var comma = key.IndexOf (','); - if (comma < 0) { - return null; - } - - var typeName = key.Substring (0, comma).Trim (); - if (typeName.Length == 0 || typeName.IndexOf ('/') >= 0) { - return null; - } - - return typeName.Replace ('+', '/'); - } - - void ReadAssemblyStoreProfile (ApkComparisonProfile profile) - { - (var explorers, var errorMessage) = AssemblyStoreExplorer.Open (profile.ApkPath); - Assert.IsNull (errorMessage, $"{profile.ApkPath} should contain readable assembly stores."); - Assert.IsNotNull (explorers, $"{profile.ApkPath} should contain assembly stores."); - - var explorer = explorers.FirstOrDefault (e => e.TargetArch == AndroidTargetArch.Arm64); - Assert.IsNotNull (explorer, $"{profile.ApkPath} should contain an arm64-v8a assembly store."); - - profile.AssemblyStoreCount = explorers.Count; - foreach (var store in explorers) { - var storeSize = store.Assemblies?.Where (a => !a.Ignore).Sum (a => (long)a.DataSize) ?? 0; - profile.AssemblyStores.Add ($"{store.TargetArch}: assemblies={store.AssemblyCount}, indexed={store.IndexEntryCount}, size={storeSize}"); - profile.AssemblyStoreSize += storeSize; - } - - foreach (var item in explorer.Assemblies.Where (a => !a.Ignore && a.Name.EndsWith (".dll", StringComparison.OrdinalIgnoreCase) && !a.Name.EndsWith (".ni.dll", StringComparison.OrdinalIgnoreCase))) { - profile.ManagedAssemblyNames.Add (item.Name); - using var stream = explorer.ReadImageData (item); - if (stream == null) { - continue; - } - - using var assemblyStream = GetManagedAssemblyStream (stream, item.Name); - AssemblyDefinition assembly; - try { - assembly = AssemblyDefinition.ReadAssembly (assemblyStream); - } catch (BadImageFormatException ex) { - Assert.Fail ($"Assembly store entry '{item.Name}' should contain a readable managed assembly: {ex.Message}. First bytes: {ReadFirstBytes (assemblyStream)}"); - throw; - } - using (assembly) { - profile.ManagedAssemblyCount++; - if (item.Name.EndsWith (".TypeMap.dll", StringComparison.Ordinal) || item.Name == "_Microsoft.Android.TypeMaps.dll") { - ReadTypeMapAssemblyProfile (profile, "packaged", assembly, item.Name); - } - foreach (var type in assembly.Modules.SelectMany (m => m.Types).SelectMany (FlattenType)) { - if (IsTypemapHelperManagedType (type.FullName)) { - continue; - } - - profile.RawManagedTypeCount++; - profile.RawManagedMethodCount += type.Methods.Count; - - if (!IsManagedTypemapEligible (type, profile)) { - continue; - } - - profile.FilteredManagedTypeCount++; - profile.FilteredManagedMethodCount += type.Methods.Count; - profile.ManagedTypemapEntries.Add ($"type {type.FullName}"); - foreach (var method in type.Methods) { - profile.ManagedTypemapEntries.Add ($"method {type.FullName}::{GetManagedMethodSignature (method)}"); - } - } - } - } - } - - void ReadTypeMapAssemblyProfile (ApkComparisonProfile profile, string stage, string directory) - { - if (!Directory.Exists (directory)) { - return; - } - - foreach (var file in Directory.EnumerateFiles (directory, "*.dll", SearchOption.TopDirectoryOnly).Where (IsTypeMapAssemblyPath)) { - using var assembly = AssemblyDefinition.ReadAssembly (file); - ReadTypeMapAssemblyProfile (profile, stage, assembly, Path.GetFileName (file)); - } - } bool IsTypeMapAssemblyPath (string file) { - var name = Path.GetFileName (file); - return name.EndsWith (".TypeMap.dll", StringComparison.Ordinal) || name == "_Microsoft.Android.TypeMaps.dll"; - } - - void ReadTypeMapAssemblyProfile (ApkComparisonProfile profile, string stage, AssemblyDefinition assembly, string assemblyName) - { - var metrics = new TypeMapAssemblyMetrics { - Stage = stage, - AssemblyName = assemblyName, - }; - - foreach (var attribute in assembly.CustomAttributes) { - var attributeName = attribute.AttributeType.FullName; - if (attributeName.StartsWith ("System.Runtime.InteropServices.TypeMapAttribute`1", StringComparison.Ordinal)) { - ReadTypeMapAttribute (attribute, metrics); - } else if (attributeName.StartsWith ("System.Runtime.InteropServices.TypeMapAssociationAttribute", StringComparison.Ordinal)) { - metrics.AssociationAttributeCount++; - } else if (attributeName.StartsWith ("System.Runtime.InteropServices.TypeMapAssemblyTargetAttribute`1", StringComparison.Ordinal)) { - metrics.AssemblyTargetAttributeCount++; - } - } - - if (metrics.TypeMapAttributeCount != 0 || metrics.AssociationAttributeCount != 0 || metrics.AssemblyTargetAttributeCount != 0) { - profile.TypeMapAssemblies.Add (metrics); - } - } - - void ReadTypeMapAttribute (CustomAttribute attribute, TypeMapAssemblyMetrics metrics) - { - metrics.TypeMapAttributeCount++; - if (attribute.ConstructorArguments.Count == 2) { - metrics.UnconditionalTypeMapAttributeCount++; - } else if (attribute.ConstructorArguments.Count == 3) { - metrics.ConditionalTypeMapAttributeCount++; - } - - var jniName = attribute.ConstructorArguments.Count > 0 ? attribute.ConstructorArguments [0].Value as string : null; - var proxyType = attribute.ConstructorArguments.Count > 1 ? attribute.ConstructorArguments [1].Value as string : null; - var targetType = attribute.ConstructorArguments.Count > 2 ? attribute.ConstructorArguments [2].Value as string : null; - var key = $"{jniName}\t{proxyType}\t{targetType}"; - metrics.TypeMapAttributeKeys.Add (key); - if (jniName != null) { - metrics.IncrementPrefixBucket (jniName); - } - } - - bool IsManagedTypemapEligible (TypeDefinition type, ApkComparisonProfile profile) - { - if (profile.CandidateManagedTypes.Contains (type.FullName)) { - return true; - } - - var isTypemapEligible = false; - foreach (var attribute in type.CustomAttributes) { - var attributeName = attribute.AttributeType.FullName; - if (attributeName != "Android.Runtime.RegisterAttribute" && attributeName != "Java.Interop.JniTypeSignatureAttribute") { - continue; - } - - isTypemapEligible = true; - if (attribute.ConstructorArguments.Count > 0 && attribute.ConstructorArguments [0].Value is string jniName) { - jniName = NormalizeJniName (jniName); - if (!IsTypemapHelperJavaType (jniName)) { - profile.CandidateJavaNames.Add (jniName); - } - } - } - - return isTypemapEligible; - } - - string GetManagedMethodSignature (MethodDefinition method) - { - var parameters = String.Join (",", method.Parameters.Select (p => p.ParameterType.FullName)); - return $"{method.Name}({parameters}):{method.ReturnType.FullName}"; - } - - string NormalizeJniName (string jniName) - { - if (jniName.Length >= 2 && jniName [0] == 'L' && jniName [jniName.Length - 1] == ';') { - return jniName.Substring (1, jniName.Length - 2); - } - - return jniName; - } - - string ReadFirstBytes (Stream stream) - { - var position = stream.Position; - stream.Seek (0, SeekOrigin.Begin); - var bytes = new byte [Math.Min (16, stream.Length)]; - stream.ReadExactly (bytes, 0, bytes.Length); - stream.Seek (position, SeekOrigin.Begin); - return BitConverter.ToString (bytes); - } - - Stream GetManagedAssemblyStream (Stream stream, string name) - { - (ulong elfPayloadOffset, ulong elfPayloadSize, var error) = Xamarin.Android.AssemblyStore.Utils.FindELFPayloadSectionOffsetAndSize (stream); - Assert.IsTrue ( - error == ELFPayloadError.None || error == ELFPayloadError.NotELF, - $"{name} should be a managed assembly or an ELF image containing one. ELF payload error: {error}"); - - if (elfPayloadOffset == 0) { - stream.Seek (0, SeekOrigin.Begin); - var copy = new MemoryStream (); - stream.CopyTo (copy); - copy.Seek (0, SeekOrigin.Begin); - return DecompressAssemblyIfNeeded (copy); - } - - var payload = new MemoryStream (); - var buffer = new byte [16 * 1024]; - var remaining = elfPayloadSize; - stream.Seek ((long)elfPayloadOffset, SeekOrigin.Begin); - while (remaining > 0) { - var read = stream.Read (buffer, 0, (int)Math.Min ((ulong)buffer.Length, remaining)); - if (read == 0) { - break; - } - payload.Write (buffer, 0, read); - remaining -= (ulong)read; - } - payload.Seek (0, SeekOrigin.Begin); - return DecompressAssemblyIfNeeded (payload); - } - - Stream DecompressAssemblyIfNeeded (Stream stream) - { - using var reader = new BinaryReader (stream, Encoding.UTF8, leaveOpen: true); - var magic = reader.ReadUInt32 (); - if (magic != CompressedAssemblyMagic) { - stream.Seek (0, SeekOrigin.Begin); - return stream; - } - - reader.ReadUInt32 (); - var decompressedLength = reader.ReadUInt32 (); - var compressedLength = (int)(stream.Length - stream.Position); - var compressed = new byte [compressedLength]; - stream.ReadExactly (compressed, 0, compressed.Length); - - var decompressed = new byte [decompressedLength]; - var decoded = LZ4Codec.Decode (compressed, 0, compressed.Length, decompressed, 0, decompressed.Length); - Assert.AreEqual ((int)decompressedLength, decoded, "Compressed assembly should decompress to the expected size."); - stream.Dispose (); - return new MemoryStream (decompressed, 0, decoded, writable: false); - } - - IEnumerable FlattenType (TypeDefinition type) - { - yield return type; - foreach (var nested in type.NestedTypes) { - foreach (var nestedType in FlattenType (nested)) { - yield return nestedType; - } - } - } - - void ReadDexProfile (ApkComparisonProfile profile) - { - using var zip = ZipFile.OpenRead (profile.ApkPath); - foreach (var entry in zip.Entries.Where (e => Regex.IsMatch (e.FullName, @"^classes(\d*)\.dex$", RegexOptions.CultureInvariant))) { - using var stream = entry.Open (); - using var memory = new MemoryStream (); - stream.CopyTo (memory); - var bytes = memory.ToArray (); - - profile.DexSize += bytes.Length; - - var dex = DexProfileReader.Read (bytes); - profile.DexFiles.Add ($"{entry.FullName}: size={bytes.Length}, classes={dex.Classes.Count}"); - profile.DexStringIdCount += dex.StringIdCount; - profile.DexTypeIdCount += dex.TypeIdCount; - profile.DexProtoIdCount += dex.ProtoIdCount; - profile.DexFieldIdCount += dex.FieldIdCount; - profile.DexMethodIdCount += dex.MethodIdCount; - profile.DexDataSize += dex.DataSize; - profile.RawJavaClassCount += dex.Classes.Count; - profile.RawJavaMethodCount += dex.Classes.Sum (c => c.Methods.Count); - - foreach (var javaClass in dex.Classes) { - profile.JavaClassNames.Add (javaClass.Name); - if (IsTypemapHelperJavaType (javaClass.Name) || !profile.CandidateJavaNames.Contains (javaClass.Name)) { - continue; - } - - profile.FilteredJavaClassCount++; - profile.FilteredJavaMethodCount += javaClass.Methods.Count; - profile.JavaTypemapEntries.Add ($"class {javaClass.Name}"); - foreach (var method in javaClass.Methods) { - profile.JavaTypemapEntries.Add ($"method {javaClass.Name}->{method}"); - } - } - } - } - - bool IsTypemapHelperManagedType (string typeName) - { - return typeName.StartsWith ("_TypeMap.", StringComparison.Ordinal) || - typeName.StartsWith ("_Microsoft.Android.TypeMaps", StringComparison.Ordinal) || - typeName.EndsWith ("/__TypeMapAnchor", StringComparison.Ordinal) || - typeName == "Android.Runtime.JavaProxyThrowable" || - typeName.IndexOf ("JavaPeerProxy", StringComparison.Ordinal) >= 0 || - typeName.IndexOf ("TypeMapProvider", StringComparison.Ordinal) >= 0 || - typeName.IndexOf ("TypeMapping", StringComparison.Ordinal) >= 0; - } - - bool IsTypemapHelperJavaType (string jniName) - { - return jniName.StartsWith ("net/dot/android/", StringComparison.Ordinal) || - jniName.StartsWith ("mono/android/", StringComparison.Ordinal) || - jniName.IndexOf ("JavaPeerProxy", StringComparison.Ordinal) >= 0 || - jniName.IndexOf ("TypeMap", StringComparison.Ordinal) >= 0; - } - - void WriteComparisonDiagnostics (ApkComparisonProfile llvmIr, ApkComparisonProfile trimmable) - { - var managedDiff = GetEntryDiff (llvmIr.ManagedTypemapEntries, trimmable.ManagedTypemapEntries); - var javaDiff = GetEntryDiff (llvmIr.JavaTypemapEntries, trimmable.JavaTypemapEntries); - var javaClassDiff = GetEntryDiff (llvmIr.JavaClassNames, trimmable.JavaClassNames); - - TestContext.Out.WriteLine ("APK contents comparison: llvm-ir vs trimmable typemap"); - WriteComparisonTable (llvmIr, trimmable, managedDiff, javaDiff); - WriteSize ("APK", llvmIr.ApkSize, trimmable.ApkSize); - WriteSize ("assembly stores", llvmIr.AssemblyStoreSize, trimmable.AssemblyStoreSize); - WriteSize ("classes*.dex", llvmIr.DexSize, trimmable.DexSize); - WriteProfile (llvmIr); - WriteProfile (trimmable); - WriteEntryDiff ("managed typemap entries", managedDiff); - WriteEntryDiff ("Java typemap entries", javaDiff); - WriteEntryDiff ("Java classes", javaClassDiff); - } - - void WriteComparisonTable (ApkComparisonProfile llvmIr, ApkComparisonProfile trimmable, EntryDiff managedDiff, EntryDiff javaDiff) - { - TestContext.Out.WriteLine ("| Metric | llvm-ir | trimmable |"); - TestContext.Out.WriteLine ("|---|---:|---:|"); - TestContext.Out.WriteLine ($"| APK size | {FormatNumber (llvmIr.ApkSize)} | {FormatNumber (trimmable.ApkSize)} |"); - TestContext.Out.WriteLine ($"| Assembly-store payload | {FormatNumber (llvmIr.AssemblyStoreSize)} | {FormatNumber (trimmable.AssemblyStoreSize)} |"); - TestContext.Out.WriteLine ($"| classes*.dex | {FormatNumber (llvmIr.DexSize)} | {FormatNumber (trimmable.DexSize)} |"); - TestContext.Out.WriteLine ($"| Registered managed types / methods | {FormatNumber (llvmIr.FilteredManagedTypeCount)} / {FormatNumber (llvmIr.FilteredManagedMethodCount)} | {FormatNumber (trimmable.FilteredManagedTypeCount)} / {FormatNumber (trimmable.FilteredManagedMethodCount)} |"); - TestContext.Out.WriteLine ($"| Managed diff | {FormatNumber (managedDiff.LlvmIrOnly.Length)} llvm-ir-only | {FormatNumber (managedDiff.TrimmableOnly.Length)} trimmable-only |"); - TestContext.Out.WriteLine ($"| Java diff | {FormatNumber (javaDiff.LlvmIrOnly.Length)} llvm-ir-only | {FormatNumber (javaDiff.TrimmableOnly.Length)} trimmable-only |"); - } - - string FormatNumber (long value) => value.ToString ("N0", CultureInfo.InvariantCulture); - - void WriteProfile (ApkComparisonProfile profile) - { - TestContext.Out.WriteLine ($"{profile.Name}: apk={profile.ApkSize} bytes, stores={profile.AssemblyStoreCount}, store-bytes={profile.AssemblyStoreSize}, dex-bytes={profile.DexSize}"); - TestContext.Out.WriteLine ($"{profile.Name}: managed assemblies={profile.ManagedAssemblyCount}, raw types={profile.RawManagedTypeCount}, raw methods={profile.RawManagedMethodCount}, filtered types={profile.FilteredManagedTypeCount}, filtered methods={profile.FilteredManagedMethodCount}"); - TestContext.Out.WriteLine ($"{profile.Name}: raw Java classes={profile.RawJavaClassCount}, raw Java methods={profile.RawJavaMethodCount}, filtered classes={profile.FilteredJavaClassCount}, filtered methods={profile.FilteredJavaMethodCount}"); - TestContext.Out.WriteLine ($"{profile.Name}: dex ids: strings={profile.DexStringIdCount}, types={profile.DexTypeIdCount}, protos={profile.DexProtoIdCount}, fields={profile.DexFieldIdCount}, methods={profile.DexMethodIdCount}, data-size={profile.DexDataSize}"); - TestContext.Out.WriteLine ($"{profile.Name}: generated Java sources={profile.GeneratedJavaSourceCount}, __md_methods files={profile.GeneratedJavaWithMdMethodsCount}, Runtime.register files={profile.GeneratedJavaWithRuntimeRegisterCount}, Runtime.registerNatives files={profile.GeneratedJavaWithRegisterNativesCount}"); - TestContext.Out.WriteLine ($"{profile.Name}: assembly stores: {String.Join ("; ", profile.AssemblyStores)}"); - TestContext.Out.WriteLine ($"{profile.Name}: dex files: {String.Join ("; ", profile.DexFiles)}"); - foreach (var metrics in profile.TypeMapAssemblies) { - TestContext.Out.WriteLine ($"{profile.Name}: typemap {metrics.Stage}/{metrics.AssemblyName}: typemap={metrics.TypeMapAttributeCount}, unique={metrics.UniqueTypeMapAttributeCount}, duplicates={metrics.DuplicateTypeMapAttributeCount}, unconditional={metrics.UnconditionalTypeMapAttributeCount}, conditional={metrics.ConditionalTypeMapAttributeCount}, associations={metrics.AssociationAttributeCount}, assembly-targets={metrics.AssemblyTargetAttributeCount}, prefixes={metrics.FormatPrefixBuckets ()}"); - } - } - - void WriteSize (string label, long llvmIr, long trimmable) - { - var ratio = llvmIr == 0 ? 0 : (double)trimmable / llvmIr; - TestContext.Out.WriteLine ($"{label}: llvm-ir={llvmIr}, trimmable={trimmable}, delta={trimmable - llvmIr}, ratio={ratio:0.000}"); - } - - EntryDiff GetEntryDiff (ISet llvmIr, ISet trimmable) - { - var llvmOnly = llvmIr.Except (trimmable, StringComparer.Ordinal).OrderBy (x => x, StringComparer.Ordinal).ToArray (); - var trimmableOnly = trimmable.Except (llvmIr, StringComparer.Ordinal).OrderBy (x => x, StringComparer.Ordinal).ToArray (); - var common = llvmIr.Intersect (trimmable, StringComparer.Ordinal).Count (); - - return new EntryDiff (llvmOnly, trimmableOnly, common); - } - - void WriteEntryDiff (string label, EntryDiff diff) - { - TestContext.Out.WriteLine ($"{label}: llvm-ir only={diff.LlvmIrOnly.Length}, trimmable only={diff.TrimmableOnly.Length}, common={diff.Common}"); - WriteSample ($"{label} llvm-ir only", diff.LlvmIrOnly); - WriteSample ($"{label} trimmable only", diff.TrimmableOnly); - } - - void WriteSample (string label, string [] entries) - { - if (entries.Length == 0) { - return; - } - - TestContext.Out.WriteLine ($"{label}:"); - foreach (var entry in entries.Take (50)) { - TestContext.Out.WriteLine ($" {entry}"); - } - if (entries.Length > 50) { - TestContext.Out.WriteLine ($" ... {entries.Length - 50} more"); - } - } - - class ApkComparisonProfile - { - public string Name; - public string ApkPath; - public long ApkSize; - public int AssemblyStoreCount; - public long AssemblyStoreSize; - public long DexSize; - public int ManagedAssemblyCount; - public int RawManagedTypeCount; - public int RawManagedMethodCount; - public int FilteredManagedTypeCount; - public int FilteredManagedMethodCount; - public int RawJavaClassCount; - public int RawJavaMethodCount; - public int FilteredJavaClassCount; - public int FilteredJavaMethodCount; - public int DexStringIdCount; - public int DexTypeIdCount; - public int DexProtoIdCount; - public int DexFieldIdCount; - public int DexMethodIdCount; - public int DexDataSize; - public int GeneratedJavaSourceCount; - public int GeneratedJavaWithMdMethodsCount; - public int GeneratedJavaWithRuntimeRegisterCount; - public int GeneratedJavaWithRegisterNativesCount; - public readonly List AssemblyStores = new List (); - public readonly List DexFiles = new List (); - public readonly HashSet ManagedAssemblyNames = new HashSet (StringComparer.Ordinal); - public readonly HashSet CandidateManagedTypes = new HashSet (StringComparer.Ordinal); - public readonly HashSet CandidateJavaNames = new HashSet (StringComparer.Ordinal); - public readonly HashSet ManagedTypemapEntries = new HashSet (StringComparer.Ordinal); - public readonly HashSet JavaTypemapEntries = new HashSet (StringComparer.Ordinal); - public readonly HashSet JavaClassNames = new HashSet (StringComparer.Ordinal); - public readonly List TypeMapAssemblies = new List (); + var fileName = Path.GetFileName (file); + return fileName.EndsWith (".TypeMap.dll", StringComparison.Ordinal) || + fileName.StartsWith ("_Microsoft.Android.TypeMap", StringComparison.Ordinal); } class DynamicCodeSupportProfile @@ -1131,291 +585,5 @@ class DynamicCodeSupportProfile public string RuntimeConfig; public bool LinkedTypeMapAssembliesContainArrayRankSentinels; } - - class TypeMapAssemblyMetrics - { - public string Stage; - public string AssemblyName; - public int TypeMapAttributeCount; - public int UnconditionalTypeMapAttributeCount; - public int ConditionalTypeMapAttributeCount; - public int AssociationAttributeCount; - public int AssemblyTargetAttributeCount; - public readonly List TypeMapAttributeKeys = new List (); - readonly SortedDictionary prefixBuckets = new SortedDictionary (StringComparer.Ordinal); - - public int UniqueTypeMapAttributeCount => TypeMapAttributeKeys.Distinct (StringComparer.Ordinal).Count (); - public int DuplicateTypeMapAttributeCount => TypeMapAttributeCount - UniqueTypeMapAttributeCount; - - public void IncrementPrefixBucket (string jniName) - { - var bucket = GetPrefixBucket (jniName); - prefixBuckets.TryGetValue (bucket, out int count); - prefixBuckets [bucket] = count + 1; - } - - public string FormatPrefixBuckets () - { - return String.Join (", ", prefixBuckets.Select (p => $"{p.Key}={p.Value}")); - } - - static string GetPrefixBucket (string jniName) - { - if (jniName.StartsWith ("mono/android/", StringComparison.Ordinal)) { - return "mono/android"; - } - if (jniName.StartsWith ("android/", StringComparison.Ordinal)) { - return "android"; - } - if (jniName.StartsWith ("java/", StringComparison.Ordinal)) { - return "java"; - } - if (jniName.StartsWith ("com/xamarin/", StringComparison.Ordinal)) { - return "app"; - } - return "other"; - } - } - - class EntryDiff - { - public EntryDiff (string [] llvmIrOnly, string [] trimmableOnly, int common) - { - LlvmIrOnly = llvmIrOnly; - TrimmableOnly = trimmableOnly; - Common = common; - } - - public string [] LlvmIrOnly { get; } - public string [] TrimmableOnly { get; } - public int Common { get; } - } - - class DexClass - { - public string Name; - public readonly List Methods = new List (); - } - - class DexProfile - { - public readonly List Classes = new List (); - public int StringIdCount { get; set; } - public int TypeIdCount { get; set; } - public int ProtoIdCount { get; set; } - public int FieldIdCount { get; set; } - public int MethodIdCount { get; set; } - public int DataSize { get; set; } - } - - class DexProfileReader - { - readonly byte [] data; - - DexProfileReader (byte [] data) - { - this.data = data; - } - - public static DexProfile Read (byte [] data) => new DexProfileReader (data).ReadProfile (); - - DexProfile ReadProfile () - { - Assert.AreEqual ((byte)'d', data [0], "classes.dex magic should start with dex."); - Assert.AreEqual ((byte)'e', data [1], "classes.dex magic should start with dex."); - Assert.AreEqual ((byte)'x', data [2], "classes.dex magic should start with dex."); - - var strings = ReadStrings (); - var typeIds = ReadTypeIds (strings); - var protoIds = ReadProtoIds (typeIds); - var methodIds = ReadMethodIds (strings, protoIds); - var profile = new DexProfile { - StringIdCount = strings.Length, - TypeIdCount = typeIds.Length, - ProtoIdCount = protoIds.Length, - FieldIdCount = ReadInt32 (80), - MethodIdCount = methodIds.Length, - DataSize = ReadInt32 (104), - }; - - var classDefsSize = ReadInt32 (96); - var classDefsOffset = ReadInt32 (100); - for (int i = 0; i < classDefsSize; i++) { - var classDefOffset = classDefsOffset + i * 32; - var classIdx = ReadInt32 (classDefOffset); - var classDataOffset = ReadInt32 (classDefOffset + 24); - var dexClass = new DexClass { - Name = DescriptorToJniName (GetItem (typeIds, classIdx, "class type")), - }; - - if (classDataOffset != 0) { - ReadClassData (classDataOffset, methodIds, dexClass); - } - - profile.Classes.Add (dexClass); - } - - return profile; - } - - string [] ReadStrings () - { - var stringsSize = ReadInt32 (56); - var stringsOffset = ReadInt32 (60); - var strings = new string [stringsSize]; - for (int i = 0; i < stringsSize; i++) { - var offset = ReadInt32 (stringsOffset + i * 4); - ReadUleb128 (ref offset); - var start = offset; - while (true) { - EnsureAvailable (offset, 1, "string data"); - if (data [offset] == 0) { - break; - } - offset++; - } - strings [i] = Encoding.UTF8.GetString (data, start, offset - start); - } - - return strings; - } - - string [] ReadTypeIds (string [] strings) - { - var typeIdsSize = ReadInt32 (64); - var typeIdsOffset = ReadInt32 (68); - var typeIds = new string [typeIdsSize]; - for (int i = 0; i < typeIdsSize; i++) { - typeIds [i] = GetItem (strings, ReadInt32 (typeIdsOffset + i * 4), "type descriptor string"); - } - - return typeIds; - } - - string [] ReadProtoIds (string [] typeIds) - { - var protoIdsSize = ReadInt32 (72); - var protoIdsOffset = ReadInt32 (76); - var protoIds = new string [protoIdsSize]; - for (int i = 0; i < protoIdsSize; i++) { - var returnType = GetItem (typeIds, ReadInt32 (protoIdsOffset + i * 12 + 4), "proto return type"); - var parametersOffset = ReadInt32 (protoIdsOffset + i * 12 + 8); - protoIds [i] = $"({ReadTypeList (parametersOffset, typeIds)}){returnType}"; - } - - return protoIds; - } - - string ReadTypeList (int offset, string [] typeIds) - { - if (offset == 0) { - return ""; - } - - var count = ReadInt32 (offset); - var builder = new StringBuilder (); - offset += 4; - for (int i = 0; i < count; i++) { - builder.Append (GetItem (typeIds, ReadUInt16 (offset + i * 2), "proto parameter type")); - } - return builder.ToString (); - } - - string [] ReadMethodIds (string [] strings, string [] protoIds) - { - var methodIdsSize = ReadInt32 (88); - var methodIdsOffset = ReadInt32 (92); - var methodIds = new string [methodIdsSize]; - for (int i = 0; i < methodIdsSize; i++) { - var protoIdx = ReadUInt16 (methodIdsOffset + i * 8 + 2); - methodIds [i] = GetItem (strings, ReadInt32 (methodIdsOffset + i * 8 + 4), "method name string") + GetItem (protoIds, protoIdx, "method prototype"); - } - - return methodIds; - } - - void ReadClassData (int offset, string [] methodIds, DexClass dexClass) - { - var staticFieldsSize = ReadUleb128 (ref offset); - var instanceFieldsSize = ReadUleb128 (ref offset); - var directMethodsSize = ReadUleb128 (ref offset); - var virtualMethodsSize = ReadUleb128 (ref offset); - - SkipEncodedFields (staticFieldsSize, ref offset); - SkipEncodedFields (instanceFieldsSize, ref offset); - ReadEncodedMethods (directMethodsSize, methodIds, dexClass, ref offset); - ReadEncodedMethods (virtualMethodsSize, methodIds, dexClass, ref offset); - } - - void SkipEncodedFields (int count, ref int offset) - { - for (int i = 0; i < count; i++) { - ReadUleb128 (ref offset); - ReadUleb128 (ref offset); - } - } - - void ReadEncodedMethods (int count, string [] methodIds, DexClass dexClass, ref int offset) - { - var methodIndex = 0; - for (int i = 0; i < count; i++) { - methodIndex += ReadUleb128 (ref offset); - ReadUleb128 (ref offset); - ReadUleb128 (ref offset); - dexClass.Methods.Add (GetItem (methodIds, methodIndex, "encoded method")); - } - } - - int ReadInt32 (int offset) - { - EnsureAvailable (offset, 4, "uint"); - return data [offset] | - (data [offset + 1] << 8) | - (data [offset + 2] << 16) | - (data [offset + 3] << 24); - } - - int ReadUInt16 (int offset) - { - EnsureAvailable (offset, 2, "ushort"); - return data [offset] | - (data [offset + 1] << 8); - } - - int ReadUleb128 (ref int offset) - { - var result = 0; - var shift = 0; - int value; - do { - EnsureAvailable (offset, 1, "ULEB128"); - value = data [offset++]; - result |= (value & 0x7f) << shift; - shift += 7; - } while ((value & 0x80) != 0); - - return result; - } - - T GetItem (T [] items, int index, string description) - { - Assert.IsTrue (index >= 0 && index < items.Length, $"Invalid {description} index {index}; table has {items.Length} entries."); - return items [index]; - } - - void EnsureAvailable (int offset, int count, string description) - { - Assert.IsTrue (offset >= 0 && count >= 0 && offset <= data.Length - count, $"DEX {description} read at offset {offset} with size {count} exceeds file size {data.Length}."); - } - - string DescriptorToJniName (string descriptor) - { - if (descriptor.Length >= 2 && descriptor [0] == 'L' && descriptor [descriptor.Length - 1] == ';') { - return descriptor.Substring (1, descriptor.Length - 2); - } - - return descriptor; - } - } } } From 70660e3e817e7e193ebfff0251b4643f11050b1f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 07:53:51 +0000 Subject: [PATCH 7/9] Address PR review feedback for trimmable typemap Agent-Logs-Url: https://github.com/dotnet/android/sessions/9991cddf-ded2-4c77-8804-19e7a4ecab36 Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 10 ---------- .../TrimmableTypeMapBuildTests.cs | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 652c4fb3a5b..5a391f520b6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -48,7 +48,6 @@ public TrimmableTypeMapResult Execute ( logger.LogNoJavaPeerTypesFound (); return new TrimmableTypeMapResult ([], [], allPeers); } - MarkFrameworkAssemblyPeers (allPeers, frameworkAssemblyNames); RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); PropagateDeferredRegistrationToBaseClasses (allPeers); @@ -213,15 +212,6 @@ List GenerateTypeMapAssemblies ( return generatedAssemblies; } - static void MarkFrameworkAssemblyPeers (List allPeers, HashSet frameworkAssemblyNames) - { - foreach (var peer in allPeers) { - if (frameworkAssemblyNames.Contains (peer.AssemblyName)) { - peer.IsFrameworkAssembly = true; - } - } - } - /// /// Groups peers by assembly, merging cross-assembly aliases into a single group. /// When the same JNI name appears in multiple assemblies (e.g. Java.Lang.Object diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 3d24e6a1ef0..e496abd3039 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; using System.Text.RegularExpressions; using Mono.Cecil; using NUnit.Framework; @@ -269,9 +270,18 @@ public void ReleaseCoreClrTrimmableTypeMap_SupportsExplicitDynamicCodeSupportOff var dynamicCodeDisabledTrimmable = BuildDynamicCodeSupportProfile ("trimmable", dynamicCodeSupport: false); - const string dynamicCodeSupportFalse = "\"System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported\": false"; + using var runtimeConfigJson = JsonDocument.Parse (dynamicCodeDisabledTrimmable.RuntimeConfig); Assert.IsTrue ( - dynamicCodeDisabledTrimmable.RuntimeConfig.Contains (dynamicCodeSupportFalse, StringComparison.Ordinal), + runtimeConfigJson.RootElement.TryGetProperty ("runtimeOptions", out var runtimeOptions), + "runtimeconfig.json should include runtimeOptions."); + Assert.IsTrue ( + runtimeOptions.TryGetProperty ("configProperties", out var configProperties), + "runtimeconfig.json should include runtimeOptions.configProperties."); + Assert.IsTrue ( + configProperties.TryGetProperty ("System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported", out var dynamicCodeSupportProperty), + "runtimeconfig.json should include RuntimeFeature.IsDynamicCodeSupported."); + Assert.IsFalse ( + dynamicCodeSupportProperty.GetBoolean (), "trimmable typemap builds should honor explicit DynamicCodeSupport=false."); Assert.IsTrue ( dynamicCodeDisabledTrimmable.LinkedTypeMapAssembliesContainArrayRankSentinels, From 2c2548175f1e5687907682f0b1deeb7c62688bb9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 26 May 2026 21:11:17 +0200 Subject: [PATCH 8/9] Fix trimmable typemap startup roots Pass the root typemap assembly itself to ILLink and configure TypeMapping to load entries from the generated root assembly before runtime typemap initialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 44 +++++++++++++----- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 8 +--- .../RootTypeMapAssemblyGeneratorTests.cs | 45 +++++++++++++++++++ 3 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index f9a688ebdc9..b54c2f535ec 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -135,7 +135,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha pe.EmitIgnoresAccessChecksToAttribute (accessTargets); // Emit TypeMapLoader class with Initialize() method - EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, maxArrayRank); + EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, maxArrayRank, assemblyName); pe.WritePE (stream); } @@ -201,7 +201,7 @@ static void EmitAssemblyTargetAttribute (PEAssemblyBuilder pe, MemberReferenceHa pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle); } - static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, int maxArrayRank) + static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, int maxArrayRank, string assemblyName) { var metadata = pe.Metadata; @@ -243,21 +243,21 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand if (maxArrayRank > 0) { var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, - initializeRef, externalDictTypeSpec, externalDictArrayTypeSpec, perAssemblyTypeMapNames, maxArrayRank); + initializeRef, externalDictTypeSpec, externalDictArrayTypeSpec, perAssemblyTypeMapNames, maxArrayRank, assemblyName); } else { var initializeRef = AddInitializeSingleNoArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithSingleTypeMapNoArrays (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); + EmitInitializeWithSingleTypeMapNoArrays (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef, assemblyName); } } else { var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); if (maxArrayRank > 0) { var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, - initializeRef, externalDictTypeSpec, proxyDictTypeSpec, externalDictArrayTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); + initializeRef, externalDictTypeSpec, proxyDictTypeSpec, externalDictArrayTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank, assemblyName); } else { var initializeRef = AddInitializeAggregateNoArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithAggregateTypeMapNoArrays (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, - initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef); + initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, assemblyName); } } } @@ -274,7 +274,8 @@ static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, TypeSpecificationHandle externalDictArrayTypeSpec, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, - int maxArrayRank) + int maxArrayRank, + string assemblyName) { var count = perAssemblyTypeMapNames.Count; @@ -292,6 +293,7 @@ static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { + EmitSetTypeMappingEntryAssembly (pe, encoder, assemblyName); // var typeMaps = new IReadOnlyDictionary[N]; (loc 0) EmitNewArrayLocal (encoder, count, externalDictTypeSpec, slot: 0); EmitFillArrayLocal (encoder, count, getExternalSpecs, slot: 0); @@ -345,7 +347,8 @@ static void EmitInitializeWithAggregateTypeMapNoArrays (PEAssemblyBuilder pe, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, MemberReferenceHandle initializeRef, TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, - TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, + string assemblyName) { var count = perAssemblyTypeMapNames.Count; @@ -363,6 +366,7 @@ static void EmitInitializeWithAggregateTypeMapNoArrays (PEAssemblyBuilder pe, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { + EmitSetTypeMappingEntryAssembly (pe, encoder, assemblyName); EmitNewArrayLocal (encoder, count, externalDictTypeSpec, slot: 0); EmitFillArrayLocal (encoder, count, getExternalSpecs, slot: 0); @@ -432,7 +436,8 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle MemberReferenceHandle initializeRef, TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle externalDictArrayTypeSpec, IReadOnlyList perAssemblyTypeMapNames, - int maxArrayRank) + int maxArrayRank, + string assemblyName) { var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); @@ -441,6 +446,7 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { + EmitSetTypeMappingEntryAssembly (pe, encoder, assemblyName); // TrimmableTypeMap.Initialize(GetExternal(), GetProxy(), arrayMapsByAssemblyAndRank-or-null) encoder.Call (getExternalSpec, parameterCount: 0, returnsValue: true); encoder.Call (getProxySpec, parameterCount: 0, returnsValue: true); @@ -456,7 +462,8 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle /// static void EmitInitializeWithSingleTypeMapNoArrays (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, - MemberReferenceHandle initializeRef) + MemberReferenceHandle initializeRef, + string assemblyName) { var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); @@ -465,6 +472,7 @@ static void EmitInitializeWithSingleTypeMapNoArrays (PEAssemblyBuilder pe, Entit MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { + EmitSetTypeMappingEntryAssembly (pe, encoder, assemblyName); encoder.Call (getExternalSpec, parameterCount: 0, returnsValue: true); encoder.Call (getProxySpec, parameterCount: 0, returnsValue: true); encoder.Call (initializeRef, parameterCount: 2); @@ -486,6 +494,22 @@ static MemberReferenceHandle AddInitializeSingleNoArraysRef (PEAssemblyBuilder p pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); } + static void EmitSetTypeMappingEntryAssembly (PEAssemblyBuilder pe, TrackedInstructionEncoder encoder, string assemblyName) + { + var appContextRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef, + pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("AppContext")); + var setDataRef = pe.AddMemberRef (appContextRef, "SetData", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().Object (); + })); + encoder.LoadString (pe.Metadata.GetOrAddUserString ("System.Runtime.InteropServices.TypeMappingEntryAssembly")); + encoder.LoadString (pe.Metadata.GetOrAddUserString (assemblyName)); + encoder.Call (setDataRef, parameterCount: 2); + } + /// MemberRef for TrimmableTypeMap.Initialize(typeMap, proxyMap, arrayMapsByAssemblyAndRank[][]). static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 6efe05d59b4..a76819a61a1 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -25,15 +25,9 @@ - - <_TrimmableTypeMapEntryAssemblies Include="$(_TypeMapOutputDirectory)*.TypeMap.dll" /> - - <_ExtraTrimmerArgs>@(_TrimmableTypeMapEntryAssemblies->'--typemap-entry-assembly %(Filename)', ' ') $(_ExtraTrimmerArgs) + <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --typemap-entry-assembly "$(_TypeMapAssemblyName)" - - <_TrimmableTypeMapEntryAssemblies Remove="@(_TrimmableTypeMapEntryAssemblies)" /> - reader.GetTypeReference (h)) + .ToList (); + Assert.Contains (typeRefs, t => + reader.GetString (t.Name) == "AppContext" && + reader.GetString (t.Namespace) == "System"); + + var setDataMemberRefs = reader.MemberReferences + .Select (h => reader.GetMemberReference (h)) + .Where (m => reader.GetString (m.Name) == "SetData") + .ToList (); + Assert.NotEmpty (setDataMemberRefs); + + var loadedStrings = GetLoadStringOperands (pe, reader, "Initialize"); + Assert.Contains ("System.Runtime.InteropServices.TypeMappingEntryAssembly", loadedStrings); + Assert.Contains ("MyRoot", loadedStrings); + } + [Fact] public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute () { @@ -392,6 +417,26 @@ static List GetIgnoresAccessChecksToValues (MetadataReader reader) return result; } + static List GetLoadStringOperands (PEReader pe, MetadataReader reader, string methodName) + { + var result = new List (); + var method = reader.GetMethodDefinition (FindMethodDefinition (reader, methodName)); + var body = pe.GetMethodBody (method.RelativeVirtualAddress); + var il = body.GetILBytes (); + if (il is null) { + throw new InvalidOperationException ($"{methodName} has no IL body."); + } + for (int i = 0; i + 4 < il.Length; i++) { + if (il [i] != 0x72) { + continue; + } + var token = BitConverter.ToInt32 (il, i + 1); + result.Add (reader.GetUserString (MetadataTokens.UserStringHandle (token))); + i += 4; + } + return result; + } + [Fact] public void Generate_MergedMode_WithArrays_ProducesValidPEAssembly () { From 1399a77b5920b4517a1424450b4816dc7a88e9af Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 28 May 2026 11:21:42 +0200 Subject: [PATCH 9/9] Address trimmable typemap review feedback Keep JavaPeerInfo.IsFrameworkAssembly init-only and make the dynamic code support test profile immutable with a valid constructor-initialized runtime config. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerInfo.cs | 2 +- .../TrimmableTypeMapBuildTests.cs | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 7382a4ed60a..752e1083157 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -48,7 +48,7 @@ public sealed record JavaPeerInfo /// Framework ACWs are generated by the SDK and can be trimmed like bindings unless /// another rule explicitly roots them. /// - public bool IsFrameworkAssembly { get; set; } + public bool IsFrameworkAssembly { get; init; } /// /// True when per-rank array typemap entries should be generated for this peer. diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index e496abd3039..148641dc855 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -553,10 +553,9 @@ DynamicCodeSupportProfile BuildDynamicCodeSupportProfile (string typemapImplemen var runtimeConfigPath = FindOutputFile (builder, proj, $"{proj.ProjectName}.runtimeconfig.json"); var linkedAssemblyDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "linked")); - return new DynamicCodeSupportProfile { - RuntimeConfig = File.ReadAllText (runtimeConfigPath), - LinkedTypeMapAssembliesContainArrayRankSentinels = TypeMapAssembliesContainType (linkedAssemblyDirectory, "__ArrayMapRank1"), - }; + return new DynamicCodeSupportProfile ( + File.ReadAllText (runtimeConfigPath), + TypeMapAssembliesContainType (linkedAssemblyDirectory, "__ArrayMapRank1")); } string FindOutputFile (ProjectBuilder builder, XamarinAndroidApplicationProject proj, string fileName) @@ -590,10 +589,8 @@ bool IsTypeMapAssemblyPath (string file) fileName.StartsWith ("_Microsoft.Android.TypeMap", StringComparison.Ordinal); } - class DynamicCodeSupportProfile - { - public string RuntimeConfig; - public bool LinkedTypeMapAssembliesContainArrayRankSentinels; - } + sealed record DynamicCodeSupportProfile ( + string RuntimeConfig, + bool LinkedTypeMapAssembliesContainArrayRankSentinels); } }