Skip to content

Commit

Permalink
feat: Add optional XAML and Bindings Trimming
Browse files Browse the repository at this point in the history
(cherry picked from commit 5d956f2)
  • Loading branch information
jeromelaban authored and mergify-bot committed Jul 27, 2021
1 parent ab42d05 commit 25835ef
Show file tree
Hide file tree
Showing 31 changed files with 1,444 additions and 99 deletions.
3 changes: 1 addition & 2 deletions build/Uno.UI.Build.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,9 @@
<Target Name="UpdateTasksSHA">

<ItemGroup>
<_Sha1Replace Include="..\src\SourceGenerators\Uno.UI.Tasks\**\*.cs" />
<_Sha1Replace Include="..\src\SourceGenerators\Uno.UI.Tasks\Uno.UI.Tasks.csproj" />
<_Sha1Replace Include="..\src\SourceGenerators\Uno.UI.Tasks\Assets\RetargetAssets.cs" />
<_Sha1Replace Include="..\src\SourceGenerators\Uno.UI.Tasks\Content\Uno.UI.Tasks.targets" />
<_Sha1Replace Include="..\src\SourceGenerators\Uno.UI.Tasks\ResourcesGenerator\ResourcesGenerationTask.cs" />
</ItemGroup>

<WriteLinesToFile
Expand Down
2 changes: 2 additions & 0 deletions build/Uno.WinUI.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,8 @@
<file src="..\src\SourceGenerators\Uno.UI.Tasks\Bin\Release\*.dll" target="buildTransitive\Uno.UI.Tasks" />
<file src="..\src\SourceGenerators\Uno.UI.Tasks\Bin\Release\*.pdb" target="buildTransitive\Uno.UI.Tasks" />

<file src="..\src\SourceGenerators\Uno.UI.Tasks\external\linker\*.*" target="tools\linker" />

<file src="Uno.WinUI.targets" target="buildTransitive\MonoAndroid" />
<file src="Uno.WinUI.targets" target="buildTransitive\xamarinios10" />
<file src="Uno.WinUI.targets" target="buildTransitive\xamarinmac20" />
Expand Down
5 changes: 5 additions & 0 deletions build/run-net6-template-tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,8 @@ for($i = 0; $i -lt $dotnetBuildNet6Configurations.Length; $i++)
}

popd

# XAML Trimming build smoke test
dotnet new unoapp-net6 -n MyAppXamlTrim
& dotnet build -c Debug MyAppXamlTrim\MyAppXamlTrim.Wasm\MyAppXamlTrim.Wasm.csproj /p:UnoXamlResourcesTrimming=true
Assert-ExitCodeIsZero
46 changes: 46 additions & 0 deletions doc/articles/features/resources-trimming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# XAML Resource Trimming

XAML Resource and Binding trimming is an optional feature used to reduce the size of the final payload of an Uno Platform application.

The trimming phase happens after the compilation phase and tries to determine which UI controls are not used explicitly, and removes the associated XAML styles. The XAML styles are found through the value specified in the `TargetType` attribute.

As of Uno 3.9, XAML Resources Trimming is only available for WebAssembly projects.

## Using XAML Resources trimming for applications

In order for an application to enable resources trimming, the following needs to be added in the project file:

```xml
<PropertyGroup>
<UnoXamlResourcesTrimming>true</UnoXamlResourcesTrimming>
</PropertyGroup>
```

## Enabling XAML Resources trimming for libraries
For libraries to be eligible for resources trimming, the `UnoXamlResourcesTrimming` tag must also be added.

## Troubleshooting

### Aggressive trimming
The XAML trimming phase may remove controls for which the use cannot be detected statically.

For instance, if your application relies on the `XamlReader` class, trimmed controls will not be available and will fail to load.

If XAML trimming is still needed, the [IL Linker configuration](using-il-linker-webassembly.md) can be adjusted to keep controls individually or by namespace.

### Size is not reduced even if enabled
The IL Linker tool is used to implement this feature, and can be [controlled with its configuration file](using-il-linker-webassembly.md).

For instance, if the linker configuration file contains `<assembly fullname="uno.ui" />`, none of the UI Controls will be excluded, and the final app size will remain close as without trimming.

Note that for libraries, Uno 3.9 or later must be used to build the library, as additional metadata needs to be added at compile time. 3.8 and earlier libraries can be used in any case, but won't be eligible for trimming and may degrade the trimming phase effect.


## Size reduction statistics

As of Uno 3.9, for a `dotnet new unoapp` created app:

| | w/o XAML Trimming | w/ XAML Trimming |
| -------------------- | ----------------- | --------------- |
| Total IL Payload | 12.9MB | 9.12 MB |
| dotnet.wasm | 53MB | 28.9MB MB |
4 changes: 4 additions & 0 deletions doc/articles/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@
href: uno-fluent-assets.md
- name: Lottie animations
href: features/Lottie.md
- name: Working with XAML Trimming
href: features/resources-trimming.md
- name: Working with cookies
href: features/working-with-cookies.md
- name: Using pointer cursors
Expand Down Expand Up @@ -464,3 +466,5 @@
href: uno-development\troubleshooting-memory-issues.md
- name: Troubleshooting Source Generation
href: uno-development\troubleshooting-source-generation.md
- name: The XAML Trimming phase
href: uno-development\Uno-UI-XAML-ResourceTrimming.md
28 changes: 28 additions & 0 deletions doc/articles/uno-development/Uno-UI-XAML-ResourceTrimming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# XAML Resource Trimming

This document provides technical details about the [XAML Resource trimming phase](../features/resources-trimming.md).

## Technical Details

In Uno, XAML is generating C# code in order to speed up the creation of the UI. This allows for compile-time optimizations, such as type conversions or `x:Bind` integration.

The drawback of this approach is that code may be bundled with an app even if it's not used. In common use cases, the IL Linker is able to remove code not referenced, but in the case of XAML resources, this code is conditionally referenced through string cases only. This makes it impossible for the linker to remove that code through out-of-the-box means.

In order to dermine what is actually used by the application, the Uno Platform tooling runs a sequence of IL Linker passes and substitutions.

In order to prepare the linking pass:
- The tooling determines the presence of the `UnoXamlResourcesTrimming` msbuild property
- During the source generation, the tooling generates a `LinkerHints` class, which contains a set of properties for all `DependencyObject` inheriting classes.
- The source generation creates XAML Resources and Bindable Metadata code that conditionally uses those classes behind `LinkerHints` properties.
- The tooling also embeds an ILLinker substitution file allowing the linker to unconditionally remove the code that conditionally references those properties. For instance, for `LinkerHints.Is_Windows_UI_Xaml_Controls_Border_Available`, any block of `if (LinkerHints.Is_Windows_UI_Xaml_Controls_Border_Available)` will be removed when the `--feature Is_Windows_UI_Xaml_Controls_Border_Available false` parameter is provided to the linker.

Then the multiple passes of IL Linker are done:
- The first pass runs the IL Linker with all XAML resources and Binding Metadata disabled by setting all `LinkerHints` properties to false. This removes all code directly associated to those Bindable Metadata and XAML Resources. This has the effect of only keeping framework code which is directly referenced from user code.
- The tooling then reads the result of the linker to determine which types in the `LinkerHints` are still available in the assemblies.
- The subsequent passes run the IL Linker with `LinkerHints` enabled only for types detected to be used during the first pass. This will enable types indirectly referenced by XAML Resources (e.g. a ScrollBar inside a ScrollViewer style) to be kept by the linker.
- The tooling then reads again the result of the linker to determine which types in the `LinkerHints` are still available in the assemblies.
- The tooling re-runs this last pass until the available types list stops changing.

The resulting `LinkerHints` types are now passed as features to the final linker pass (the one bundled with the .NET tooling) to generate the final binary, containing only the used types.

As of Uno 3.9, the Uno.UI WebAssembly assembly is 7.5MB, trimmed down to 3.1MB for a `dotnet new unoapp` template.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Microsoft.CodeAnalysis.CSharp;
using System.Reflection.Metadata.Ecma335;
using Uno.UI.SourceGenerators.Helpers;
using System.Xml;

#if NETFRAMEWORK
using Uno.SourceGeneration;
Expand Down Expand Up @@ -49,8 +50,10 @@ class Generator
private string? _defaultNamespace;

private Dictionary<INamedTypeSymbol, GeneratedTypeInfo> _typeMap = new Dictionary<INamedTypeSymbol, GeneratedTypeInfo>();
private Dictionary<string, (string type, List<string> members)> _substitutions = new Dictionary<string, (string type, List<string> members)>();
private INamedTypeSymbol[]? _bindableAttributeSymbol;
private ITypeSymbol? _dependencyPropertySymbol;
private INamedTypeSymbol? _dependencyObjectSymbol;
private INamedTypeSymbol? _objectSymbol;
private INamedTypeSymbol? _javaObjectSymbol;
private INamedTypeSymbol? _nsObjectSymbol;
Expand All @@ -59,6 +62,12 @@ class Generator
private IModuleSymbol? _currentModule;
private IReadOnlyDictionary<string, INamedTypeSymbol[]>? _namedSymbolsLookup;
private INamedTypeSymbol? _stringSymbol;
private string? _projectFullPath;
private string? _projectDirectory;
private string? _baseIntermediateOutputPath;
private string? _intermediatePath;
private string? _assemblyName;
private bool _xamlResourcesTrimming;

public string[]? AnalyzerSuppressions { get; set; }

Expand All @@ -68,18 +77,35 @@ internal void Generate(GeneratorExecutionContext context)
{
var validPlatform = PlatformHelper.IsValidPlatform(context);
var isDesignTime = DesignTimeHelper.IsDesignTime(context);
var isApplication = IsApplication(context);
var isApplication = Helpers.IsApplication(context);

if (validPlatform
&& !isDesignTime)
{
if (isApplication)
{
_projectFullPath = context.GetMSBuildPropertyValue("MSBuildProjectFullPath");
_projectDirectory = Path.GetDirectoryName(_projectFullPath)
?? throw new InvalidOperationException($"MSBuild property MSBuildProjectFullPath value {_projectFullPath} is not valid");

if(!bool.TryParse(context.GetMSBuildPropertyValue("UnoXamlResourcesTrimming"), out _xamlResourcesTrimming))
{
_xamlResourcesTrimming = false;
}

_baseIntermediateOutputPath = context.GetMSBuildPropertyValue("BaseIntermediateOutputPath");
_intermediatePath = Path.Combine(
_projectDirectory,
_baseIntermediateOutputPath
);
_assemblyName = context.GetMSBuildPropertyValue("AssemblyName");

_defaultNamespace = context.GetMSBuildPropertyValue("RootNamespace");
_namedSymbolsLookup = context.Compilation.GetSymbolNameLookup();

_bindableAttributeSymbol = FindBindableAttributes(context);
_dependencyPropertySymbol = context.Compilation.GetTypeByMetadataName(XamlConstants.Types.DependencyProperty);
_dependencyObjectSymbol = context.Compilation.GetTypeByMetadataName(XamlConstants.Types.DependencyObject);

_objectSymbol = context.Compilation.GetTypeByMetadataName("System.Object");
_javaObjectSymbol = context.Compilation.GetTypeByMetadataName("Java.Lang.Object");
Expand All @@ -100,6 +126,8 @@ from module in sym.Modules
modules = modules.Concat(context.Compilation.SourceModule);

context.AddSource("BindableMetadata", GenerateTypeProviders(modules));

GenerateLinkerSubstitutionDefinition();
}
else
{
Expand All @@ -119,23 +147,10 @@ from module in sym.Modules
this.Log().Error("Failed to generate type providers.", new Exception("Failed to generate type providers." + message, e));
}
}

private INamedTypeSymbol[] FindBindableAttributes(GeneratorExecutionContext context) =>
_namedSymbolsLookup!.TryGetValue("BindableAttribute", out var types) ? types : new INamedTypeSymbol[0];

private bool IsApplication(GeneratorExecutionContext context)
{
var isAndroidApp = context.GetMSBuildPropertyValue("AndroidApplication")?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false;
var isiOSApp = context.GetMSBuildPropertyValue("ProjectTypeGuidsProperty")?.Equals("{FEACFBD2-3405-455C-9665-78FE426C6842},{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", StringComparison.OrdinalIgnoreCase) ?? false;
var ismacOSApp = context.GetMSBuildPropertyValue("ProjectTypeGuidsProperty")?.Equals("{A3F8F2AB-B479-4A4A-A458-A89E7DC349F1},{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", StringComparison.OrdinalIgnoreCase) ?? false;
var isExe = context.GetMSBuildPropertyValue("OutputType")?.Equals("Exe", StringComparison.OrdinalIgnoreCase) ?? false;
var isUnoHead = context.GetMSBuildPropertyValue("IsUnoHead")?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false;

return isAndroidApp
|| (isiOSApp && isExe)
|| (ismacOSApp && isExe)
|| isUnoHead;
}

private string GenerateTypeProviders(IEnumerable<IModuleSymbol> modules)
{
var q = from module in modules
Expand Down Expand Up @@ -211,10 +226,7 @@ private void GenerateProviderTable(IEnumerable<INamedTypeSymbol> q, IndentedStri
);
}

using (writer.BlockInvariant("static BindableMetadataProvider()"))
{
GenerateTypeTable(writer, q);
}
GenerateTypeTable(writer, q);

writer.AppendLineInvariant(@"#if DEBUG");
writer.AppendLineInvariant(@"private global::System.Collections.Generic.List<global::System.Type> _knownMissingTypes = new global::System.Collections.Generic.List<global::System.Type>();");
Expand Down Expand Up @@ -394,6 +406,8 @@ where field.IsStatic
writer.AppendLineInvariant("[System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Maintainability\", \"CA1505:AvoidUnmaintainableCode\", Justification = \"Must be ignored even if generated code is checked.\")]");
using (writer.BlockInvariant("internal static global::Uno.UI.DataBinding.IBindableType Build(global::Uno.UI.DataBinding.BindableType parent)"))
{
RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, "Uno.UI.DataBinding.IBindableType Build(Uno.UI.DataBinding.BindableType)");

writer.AppendLineInvariant(
@"var bindableType = parent ?? new global::Uno.UI.DataBinding.BindableType({0}, typeof({1}));",
flattenedProperties
Expand All @@ -418,6 +432,8 @@ where field.IsStatic
{
writer.AppendLineInvariant(@"bindableType.AddActivator(CreateInstance);");
postWriter.AppendLineInvariant($@"private static object CreateInstance() => new {ownerTypeName}();");

RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, "System.Object CreateInstance()");
}
}

Expand All @@ -431,10 +447,12 @@ where field.IsStatic
writer.AppendLineInvariant("bindableType.AddIndexer(GetIndexer, SetIndexer);");

postWriter.AppendLineInvariant($@"private static object GetIndexer(object instance, string name) => (({ownerTypeName})instance)[name];");
RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, "System.Object GetIndexer(System.Object, System.String)");

if (property.SetMethod != null)
{
postWriter.AppendLineInvariant($@"private static void SetIndexer(object instance, string name, object value) => (({ownerTypeName})instance)[name] = ({propertyTypeName})value;");
RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, "System.Void SetIndexer(System.Object,System.String,System.Object)");
}
else
{
Expand Down Expand Up @@ -477,12 +495,18 @@ where field.IsStatic
postWriter.AppendLineInvariant($@"private static object Get{propertyName}(object instance, Windows.UI.Xaml.DependencyPropertyValuePrecedences? precedence) => (({ownerTypeName})instance).{propertyName};");
postWriter.AppendLineInvariant($@"private static void Set{propertyName}(object instance, object value, Windows.UI.Xaml.DependencyPropertyValuePrecedences? precedence) => (({ownerTypeName})instance).{propertyName} = ({propertyTypeName})value;");
}

RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, $"System.Object Get{propertyName}(System.Object,System.Nullable`1<Windows.UI.Xaml.DependencyPropertyValuePrecedences>)");
RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, $"System.Void Set{propertyName}(System.Object,System.Object,System.Nullable`1<Windows.UI.Xaml.DependencyPropertyValuePrecedences>)");

}
else if (HasPublicGetter(property))
{
writer.AppendLineInvariant($@"bindableType.AddProperty(""{propertyName}"", typeof({propertyTypeName}), Get{propertyName});");

postWriter.AppendLineInvariant($@"private static object Get{propertyName}(object instance, Windows.UI.Xaml.DependencyPropertyValuePrecedences? precedence) => (({ownerTypeName})instance).{propertyName};");

RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, $"System.Object Get{propertyName}(System.Object,System.Nullable`1<Windows.UI.Xaml.DependencyPropertyValuePrecedences>)");
}
}

Expand Down Expand Up @@ -515,6 +539,17 @@ where field.IsStatic
writer.AppendLine();
}

private void RegisterHintMethod(string type, INamedTypeSymbol targetType, string signature)
{
type = _defaultNamespace + "." + type;

if (!_substitutions.TryGetValue(type, out var hint))
{
_substitutions[type] = hint = (LinkerHintsHelpers.GetPropertyAvailableName(targetType.ToDisplayString()), new List<string>());
}

hint.members.Add(signature);
}

private static string ExpandType(INamedTypeSymbol ownerType)
{
Expand Down Expand Up @@ -621,14 +656,86 @@ private string SanitizeTypeName(string name)

private void GenerateTypeTable(IndentedStringBuilder writer, IEnumerable<INamedTypeSymbol> types)
{
using (writer.BlockInvariant("static BindableMetadataProvider()"))
{
foreach (var type in _typeMap.Where(k => !k.Key.IsGenericType))
{
writer.AppendLineInvariant($"RegisterBuilder{type.Value.Index:000}();");
}
}

foreach (var type in _typeMap.Where(k => !k.Key.IsGenericType))
{
writer.AppendLineInvariant(
"_bindableTypeCacheByFullName[\"{0}\"] = CreateMemoized(MetadataBuilder_{1:000}.Build);",
type.Key,
type.Value.Index
);
using (writer.BlockInvariant($"static void RegisterBuilder{type.Value.Index:000}()"))
{
if (_xamlResourcesTrimming && type.Key.GetAllInterfaces().Any(i => SymbolEqualityComparer.Default.Equals(i, _dependencyObjectSymbol)))
{
var linkerHintsClassName = LinkerHintsHelpers.GetLinkerHintsClassName(_defaultNamespace);
var safeTypeName = LinkerHintsHelpers.GetPropertyAvailableName(type.Key.GetFullMetadataName());

writer.AppendLineInvariant($"if(global::{linkerHintsClassName}.{safeTypeName})");
}

writer.AppendLineInvariant(
"_bindableTypeCacheByFullName[\"{0}\"] = CreateMemoized(MetadataBuilder_{1:000}.Build);",
type.Key,
type.Value.Index
);
}
}
}

private void GenerateLinkerSubstitutionDefinition()
{
if (!_xamlResourcesTrimming)
{
return;
}

// <linker>
// <assembly fullname="Uno.UI">
// <type fullname="Uno.UI.GlobalStaticResources">
// <method signature="System.Void Initialize()" body="remove" />
// <method signature="System.Void RegisterDefaultStyles()" body="remove" />
// </type>
// </assembly>
// </linker>
var doc = new XmlDocument();

var xmlDeclaration = doc.CreateXmlDeclaration("1.0", "UTF-8", null);

var root = doc.DocumentElement;
doc.InsertBefore(xmlDeclaration, root);

var linkerNode = doc.CreateElement(string.Empty, "linker", string.Empty);
doc.AppendChild(linkerNode);

var assemblyNode = doc.CreateElement(string.Empty, "assembly", string.Empty);
assemblyNode.SetAttribute("fullname", _assemblyName);
linkerNode.AppendChild(assemblyNode);


foreach(var substitution in _substitutions)
{
var typeNode = doc.CreateElement(string.Empty, "type", string.Empty);
typeNode.SetAttribute("fullname", substitution.Key);
typeNode.SetAttribute("feature", substitution.Value.type);
typeNode.SetAttribute("featurevalue", "false");
assemblyNode.AppendChild(typeNode);

foreach(var method in substitution.Value.members)
{
var methodNode = doc.CreateElement(string.Empty, "method", string.Empty);
methodNode.SetAttribute("signature", method);
methodNode.SetAttribute("body", "remove");
typeNode.AppendChild(methodNode);
}
}

var fileName = Path.Combine(_intermediatePath, "Substitutions", "BindableMetadata.Substitutions.xml");
Directory.CreateDirectory(Path.GetDirectoryName(fileName));

doc.Save(fileName);
}

}
Expand Down
Loading

0 comments on commit 25835ef

Please sign in to comment.