diff --git a/doc/features-profiled-aot-validation.md b/doc/features-profiled-aot-validation.md
new file mode 100644
index 000000000..8ab334030
--- /dev/null
+++ b/doc/features-profiled-aot-validation.md
@@ -0,0 +1,71 @@
+---
+uid: Uno.Wasm.Bootstrap.ProfiledAOTValidation
+---
+
+# Troubleshooting Profiled AOT
+
+The .NET WebAssembly AOT compiler uses the AOT profile to determine which methods to compile to WebAssembly. In some cases, which may change depending on the .NET SDK version, selected methods may not be AOT compiled silently and may fall back to the interpreter.
+
+In such cases, the runtime performance of the app may become slower than expected.
+
+The bootstrapper provides a way to log the methods that were not AOT compiled, by setting the following property:
+
+```xml
+
+ true
+
+```
+
+## Assemblies filtering
+
+Assemblies may be skipped entirely from the validation using the following property:
+
+```xml
+
+ $(WasmShellAOTProfileValidationExcludedAssemblies);System.*
+
+```
+
+Entries in the `WasmShellAOTProfileValidationExcludedAssemblies` property are semi-colon separated regular expressions.
+
+## Methods filtering
+
+Specific methods may be skipped entirely from the validation using the following property:
+
+```xml
+
+ $(WasmShellAOTProfileValidationExcludedMethods);MyNamespace.MyType.MyMethod.*
+
+```
+
+Entries in the `WasmShellAOTProfileValidationExcludedMethods` property are semi-colon separated regular expressions.
+
+## Conditions when methods are not AOT compiled
+
+### Methods with try/catch/finally blocks
+
+Methods containing `try/catch/finally` blocks are not AOT compiled. `try/finally` and `try/catch` blocks are not impacted.
+
+When this pattern is needed, it's best to separate place the `try/finally` and the `try/catch` in separate methods.
+
+## Build Errors
+
+### UNOW0001
+
+The following error may be raised:
+
+```text
+UNOW0001: Method XXX from YYY has not been AOTed, even if present in the AOT profile.
+```
+
+This error is raised when the AOT profile requested a method to be AOT compiled, but was not.
+
+### UNOW0002
+
+The following error may be raised:
+
+```text
+UNOW0002: The method XXX from YYY is not present in the assembly.
+```
+
+This error generally means that there's a problem in the bootstrapper when matching methods from the compiled assemblies. If you find this specific error, please report it by opening an issue.
diff --git a/doc/runtime-execution-modes.md b/doc/runtime-execution-modes.md
index e220b410f..54e79e3ee 100644
--- a/doc/runtime-execution-modes.md
+++ b/doc/runtime-execution-modes.md
@@ -106,7 +106,10 @@ To create a profiled build:
- Build you application again
-Note that the AOT profile is a snapshot of the current set of assemblies and methods in your application. If that set changes significantly, you'll need to re-create the AOT profile to get optimal results.
+> [!NOTE]
+> AOT profile is a snapshot of the current set of assemblies and methods in your application. If that set changes significantly, you'll need to re-create the AOT profile to get optimal results.
+
+More information about [troubleshooting the Profiled AOT mode](xref:Uno.Wasm.Bootstrap.ProfiledAOTValidation) is available.
### AOT Profile method exclusion
@@ -151,20 +154,6 @@ At this time, it is only possible to exclude assemblies from being compiled to W
Adding assemblies to this list will exclude them from being compiled to WebAssembly.
-### Troubleshooting Mixed AOT/Interpreter Mode
-
-When using the Mixed AOT/Interpreter mode, it is possible that some methods may not be compiled to WebAssembly for a variety of reasons. This can cause performance issues, as the interpreter is slower than the AOT-generated code.
-
-In order to determine which methods are still using the interpreter, you can use the following property:
-
-```xml
-
- true
-
-```
-
-The logs from the AOT compiler can be found in [binlogs generated](https://aka.platform.uno/msbuild-troubleshoot) from the build.
-
### Increasing the Initial Memory Size
When building with Mixed AOT/Interpreter modes, the initial memory may need to be adjusted in the project configuration if the following error message appears:
diff --git a/src/Uno.Wasm.Bootstrap/AOTProfileValidationTask.cs b/src/Uno.Wasm.Bootstrap/AOTProfileValidationTask.cs
new file mode 100644
index 000000000..f115fc28b
--- /dev/null
+++ b/src/Uno.Wasm.Bootstrap/AOTProfileValidationTask.cs
@@ -0,0 +1,372 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Transactions;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.Profiler.Aot;
+
+namespace Uno.Wasm.Bootstrap
+{
+ public class AOTProfileValidationTask_v0 : Task
+ {
+ [Required]
+ public string AotProfile { get; set; } = "";
+
+ [Required]
+ public ITaskItem[] ILTrimmedAssemblies { get; set; } = [];
+
+ public ITaskItem[] ExcludedMethods { get; set; } = [];
+
+ public ITaskItem[] ExcludedAssemblies { get; set; } = [];
+
+ [Required]
+ public bool FailOnErrors { get; set; }
+
+ public bool ShowDebug { get; set; }
+
+ public override bool Execute()
+ {
+ Debugger.Launch();
+
+ var reader = new Mono.Profiler.Aot.ProfileReader();
+ using FileStream stream = File.OpenRead(AotProfile);
+
+ var methodFilters = ExcludedMethods
+ .Select(m => new Regex(m.ItemSpec, RegexOptions.IgnoreCase | RegexOptions.Compiled))
+ .ToArray();
+
+ var profile = reader.ReadAllData(stream);
+
+ foreach (var asm in ILTrimmedAssemblies)
+ {
+ if (ExcludedAssemblies.Any(a => Regex.IsMatch(asm.GetMetadata("FileName"), a.ItemSpec, RegexOptions.IgnoreCase)))
+ {
+ LogDebug($"Skipping filtered assembly {asm.ItemSpec})");
+ continue;
+ }
+
+ var assembly = AssemblyDefinition.ReadAssembly(asm.ItemSpec);
+
+ LogDebug($"Processing assembly {assembly.FullName})");
+
+ var asmTypes = assembly.MainModule.Types.ToDictionary(t => t.FullName, t => t);
+ var profileTypes= profile.Methods.GroupBy(m => m.Type.FullName);
+
+ foreach(var profileType in profileTypes)
+ {
+ if (!asmTypes.TryGetValue(profileType.Key, out var asmType))
+ {
+ continue;
+ }
+
+ Dictionary> typeMethods = new();
+
+ foreach(var method in asmType.Methods)
+ {
+ if (!typeMethods.TryGetValue(method.Name, out var methods))
+ {
+ methods = new List();
+ typeMethods[method.Name] = methods;
+ }
+ methods.Add(method);
+ }
+
+ foreach (var profileMethod in profileType)
+ {
+ if (profileMethod.Type.Module.Name != assembly.Name.Name)
+ {
+ continue;
+ }
+
+ if (methodFilters.Any(f => f.IsMatch(profileMethod.Name)))
+ {
+ LogDebug($"Skipping filtered method {profileMethod.Type}.{profileMethod.Name})");
+ continue;
+ }
+
+ if (profileMethod.GenericInst is not null)
+ {
+ continue;
+ }
+
+ if (typeMethods.TryGetValue(profileMethod.Name, out var methods))
+ {
+ LogDebug($"Processing profile method {profileMethod.Type}.{profileMethod.Name} ({profileMethod.Signature}, generic: {profileMethod.GenericInst?.Id ?? -1})");
+
+ var found = false;
+ var genericMethodsCount = 0;
+ foreach (var method in methods)
+ {
+ if(method.HasGenericParameters)
+ {
+ genericMethodsCount++;
+ continue;
+ }
+
+ var s = GetProfileMethodCompatibleSignature(method);
+
+ LogDebug($"Try matching method {s} with {profileMethod.Signature}");
+
+ if (s == profileMethod.Signature)
+ {
+ found = true;
+ if (!IsEmptyBody(method.Body))
+ {
+ LogError("UNOW0001", $"Method {method.FullName} from {asm.GetMetadata("FileName")} has not been AOTed, even if present in the AOT profile.");
+ }
+ else
+ {
+ LogDebug($"The method {method.FullName} from {asm.GetMetadata("FileName")} was AOTed properly");
+ }
+ }
+ }
+
+ if (!found)
+ {
+ if (genericMethodsCount != methods.Count)
+ {
+ LogError("UNOW0002", $"The method {profileMethod.Type}.{profileMethod.Name} ({profileMethod.Signature}) from {asm.GetMetadata("FileName")} is not present in the assembly.");
+ }
+ else
+ {
+ LogDebug($"Skipped all generic methods for {profileMethod.Name} in {assembly.FullName}");
+ }
+ }
+ }
+ else
+ {
+ LogDebug($"Method {profileMethod.Name} cannot be found in {assembly.FullName} assembly");
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private bool IsEmptyBody(MethodBody body)
+ {
+ if (body.Instructions.Count == 1)
+ {
+ // Some methods are empty, but have a single `ret` instruction that optimized away by the AOT compiler.
+ var instruction = body.Instructions[0];
+ if (instruction.OpCode == OpCodes.Ret)
+ {
+ return true;
+ }
+ }
+
+ // check if only invokes the default base class ctor
+ if (body.Instructions.Count == 3)
+ {
+ var instruction = body.Instructions[0];
+ if (instruction.OpCode == OpCodes.Ldarg_0)
+ {
+ instruction = body.Instructions[1];
+ if (instruction.OpCode == OpCodes.Call)
+ {
+ var method = (MethodReference)instruction.Operand;
+ if (method.Name == ".ctor")
+ {
+ instruction = body.Instructions[2];
+ if (instruction.OpCode == OpCodes.Ret)
+ {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ // check if the function only returns a constant
+ if (body.Instructions.Count == 2)
+ {
+ var instruction = body.Instructions[0];
+ if (instruction.OpCode == OpCodes.Ldc_I4_0 || instruction.OpCode == OpCodes.Ldarg_1)
+ {
+ instruction = body.Instructions[1];
+ if (instruction.OpCode == OpCodes.Ret)
+ {
+ return true;
+ }
+ }
+ }
+
+ // check if the function only returns null
+ if (body.Instructions.Count == 2)
+ {
+ var instruction = body.Instructions[0];
+ if (instruction.OpCode == OpCodes.Ldnull)
+ {
+ instruction = body.Instructions[1];
+ if (instruction.OpCode == OpCodes.Ret)
+ {
+ return true;
+ }
+ }
+ }
+
+ // Special case for a IL Strip issue where a method with
+ // EH clauses will not be stripped, but is AOT compiled anyways.
+ // If the method has a try/catch/finally, then it's considered
+ // not empty: https://github.com/dotnet/runtime/issues/111281
+ if (body.HasExceptionHandlers)
+ {
+ var hasFinally = body.ExceptionHandlers.Any(h => h.HandlerType == ExceptionHandlerType.Finally);
+ var hasCatch = body.ExceptionHandlers.Any(h => h.HandlerType == ExceptionHandlerType.Catch);
+
+ if(hasFinally && hasCatch)
+ {
+ return false;
+ }
+
+ // try/finally or try/catch are considered AOT compiled
+ return true;
+ }
+
+ // Arbitrarily set the size to 10, most remaining methods that are not stripped
+ // are usually very small, and are not worth the trouble of checking.
+ return body.CodeSize < 10;
+ }
+
+ private string GetProfileMethodCompatibleSignature(MethodDefinition method)
+ {
+ string GetLiteralType(TypeReference type)
+ {
+ var originalType = type;
+ var typeName = type.FullName;
+
+ if (type is RequiredModifierType modType)
+ {
+ type = modType.ElementType;
+ typeName = type.FullName;
+ }
+
+ if (type is ByReferenceType refType)
+ {
+ type = refType.ElementType;
+ typeName = type.FullName;
+ }
+
+ if (type is PointerType ptrType)
+ {
+ type = ptrType.ElementType;
+ typeName = type.FullName;
+ }
+
+ if (type.IsGenericParameter)
+ {
+ typeName = type.Name;
+ }
+ else if (type.IsArray)
+ {
+ typeName = GetLiteralType(((ArrayType)type).ElementType) + "[]";
+ }
+ else if (type.IsGenericInstance)
+ {
+ var gi = (GenericInstanceType)type;
+ var index = type.FullName.IndexOf('`');
+
+ typeName = type.FullName.Substring(0, index + 2) + "<" + string.Join(", ", gi.GenericArguments.Select(GetLiteralType)) + ">";
+ }
+ else
+ {
+ typeName = type.FullName.TrimEnd('&','*') switch
+ {
+ "System.Void" => "void",
+ "System.Int32" => "int",
+ "System.Int64" => "long",
+ "System.Single" => "single",
+ "System.Double" => "double",
+ "System.Boolean" => "bool",
+ "System.String" => "string",
+ "System.Byte" => "byte",
+ "System.Char" => "char",
+ "System.UInt32" => "uint",
+ "System.UInt64" => "ulong",
+ "System.SByte" => "sbyte",
+ "System.UInt16" => "uint16",
+ "System.Int16" => "int16",
+ "System.Object" => "object",
+ "System.IntPtr" => "intptr",
+ "System.UIntPtr" => "uintptr",
+ _ => type.FullName,
+ };
+ }
+
+ string RestorePtrAndRefs(TypeReference type)
+ {
+ if (type is RequiredModifierType modType)
+ {
+ return $" modreq({modType.ModifierType.FullName})" + RestorePtrAndRefs(modType.ElementType);
+ }
+ if (type is ByReferenceType byRef)
+ {
+ return RestorePtrAndRefs(byRef.ElementType) + "&";
+ }
+ else if (type is PointerType ptrRef)
+ {
+ return RestorePtrAndRefs(ptrRef.ElementType) + "*";
+ }
+ else
+ {
+ return "";
+ }
+ }
+
+ if(typeName != originalType.FullName)
+ {
+ typeName += RestorePtrAndRefs(originalType);
+ }
+
+ return typeName;
+ }
+
+
+ var sb = new StringBuilder();
+ sb.Append(GetLiteralType(method.ReturnType));
+ sb.Append("(");
+ foreach (var p in method.Parameters)
+ {
+ sb.Append(GetLiteralType(p.ParameterType));
+ sb.Append(",");
+ }
+ if (method.Parameters.Count > 0)
+ {
+ sb.Length--;
+ }
+ sb.Append(")");
+ return sb.ToString();
+ }
+
+ private void LogDebug(string message)
+ {
+ if (ShowDebug)
+ {
+ Log.LogMessage(MessageImportance.Low, message);
+ }
+ }
+
+ private void LogError(string code, string message)
+ {
+ if (FailOnErrors)
+ {
+ Log.LogMessage(MessageImportance.Low, message);
+ Log.LogError(null, code, "", null, 0, 0, 0, 0, message);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, message);
+ Log.LogWarning(null, code, "", null, 0, 0, 0, 0, message);
+ }
+ }
+ }
+}
diff --git a/src/Uno.Wasm.Bootstrap/build/Uno.Wasm.Bootstrap.props b/src/Uno.Wasm.Bootstrap/build/Uno.Wasm.Bootstrap.props
index eb2f351ab..780c03671 100644
--- a/src/Uno.Wasm.Bootstrap/build/Uno.Wasm.Bootstrap.props
+++ b/src/Uno.Wasm.Bootstrap/build/Uno.Wasm.Bootstrap.props
@@ -7,5 +7,5 @@
-
+
diff --git a/src/Uno.Wasm.Bootstrap/build/Uno.Wasm.Bootstrap.targets b/src/Uno.Wasm.Bootstrap/build/Uno.Wasm.Bootstrap.targets
index a9e5b5ed6..128028c1e 100644
--- a/src/Uno.Wasm.Bootstrap/build/Uno.Wasm.Bootstrap.targets
+++ b/src/Uno.Wasm.Bootstrap/build/Uno.Wasm.Bootstrap.targets
@@ -17,6 +17,12 @@
browser
false
+
+ false
+ false
+
+
+ $(WasmShellAOTProfileValidationExcludedAssemblies);System.*
@@ -252,11 +258,13 @@
+
+
@@ -329,7 +337,20 @@
>true
+
+
+
+
+
true
$(DefineConstants);__WASM__;UWP
app.webmanifest
-
+
+ true
+
true
True
true
@@ -17,8 +19,18 @@
$(MSBuildThisFileDirectory)aot.profile
+
+ true
+ true
+
+
+ false
+ true
+
+
+
diff --git a/src/Uno.Wasm.Sample.RayTracer/aot.profile b/src/Uno.Wasm.Sample.RayTracer/aot.profile
index 8c7d055ac..f415c4424 100644
Binary files a/src/Uno.Wasm.Sample.RayTracer/aot.profile and b/src/Uno.Wasm.Sample.RayTracer/aot.profile differ