Skip to content

Commit 9a24b8f

Browse files
Improve XAML SourceGenerator performance with C# hot reload support (dotnet#32870)
- Refactor XamlGenerator to reduce allocations - Simplify InitializeComponentCodeWriter - Remove unused tracking name - Add XTypeMultiFileHotReloadTests for multi-file hot reload scenarios Performance: ~3% improvement in XamlGenerator build time (1232ms → 1196ms mean)
1 parent 632822d commit 9a24b8f

8 files changed

Lines changed: 850 additions & 22 deletions

src/Controls/src/SourceGen/CompilationReferencesComparer.cs

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,123 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Text;
45

56
using Microsoft.CodeAnalysis;
67

78
namespace Microsoft.Maui.Controls.SourceGen;
89

9-
class CompilationReferencesComparer : IEqualityComparer<Compilation>
10+
/// <summary>
11+
/// Compares compilations by their public API signatures (types, members, method signatures).
12+
/// Implementation changes (method bodies) are ignored, so editing code inside a method
13+
/// won't trigger XAML regeneration.
14+
///
15+
/// This is slower than a references-only comparer (~1ms for 100 files vs ~0.001ms)
16+
/// but avoids regenerating all XAML files on every C# keystroke while still detecting
17+
/// signature changes that could affect XAML (new types, changed members, etc.).
18+
///
19+
/// Performance characteristics:
20+
/// - 10 files: ~0.1ms per comparison
21+
/// - 50 files: ~0.9ms per comparison
22+
/// - 100 files: ~1.1ms per comparison
23+
///
24+
/// The comparer triggers XAML regeneration when:
25+
/// - A new type is added or removed
26+
/// - A type's base class or interfaces change
27+
/// - A public/internal member is added, removed, or has its signature changed
28+
/// - External references change
29+
///
30+
/// The comparer does NOT trigger regeneration for:
31+
/// - Method body changes (implementation details)
32+
/// - Comment changes
33+
/// - Whitespace changes
34+
/// - Private member changes
35+
/// </summary>
36+
class CompilationSignaturesComparer : IEqualityComparer<Compilation>
1037
{
1138
public bool Equals(Compilation x, Compilation y)
1239
{
40+
if (ReferenceEquals(x, y))
41+
return true;
42+
1343
if (x.AssemblyName != y.AssemblyName
1444
|| x.ExternalReferences.Length != y.ExternalReferences.Length)
1545
return false;
1646

17-
return x.ExternalReferences.OfType<PortableExecutableReference>().SequenceEqual(y.ExternalReferences.OfType<PortableExecutableReference>());
47+
if (!x.ExternalReferences.OfType<PortableExecutableReference>().SequenceEqual(y.ExternalReferences.OfType<PortableExecutableReference>()))
48+
return false;
49+
50+
// Compare type signatures (ignoring method implementations)
51+
return GetSignatureString(x) == GetSignatureString(y);
52+
}
53+
54+
private static string GetSignatureString(Compilation compilation)
55+
{
56+
var sb = new StringBuilder();
57+
AppendNamespace(sb, compilation.Assembly.GlobalNamespace);
58+
return sb.ToString();
59+
}
60+
61+
private static void AppendNamespace(StringBuilder sb, INamespaceSymbol ns)
62+
{
63+
foreach (var type in ns.GetTypeMembers().OrderBy(t => t.Name))
64+
AppendType(sb, type);
65+
foreach (var child in ns.GetNamespaceMembers().OrderBy(n => n.Name))
66+
AppendNamespace(sb, child);
67+
}
68+
69+
private static void AppendType(StringBuilder sb, INamedTypeSymbol type)
70+
{
71+
sb.Append(type.DeclaredAccessibility).Append(' ');
72+
sb.Append(type.TypeKind).Append(' ');
73+
sb.Append(type.ToFQDisplayString());
74+
75+
if (type.BaseType != null && type.BaseType.SpecialType != SpecialType.System_Object)
76+
sb.Append(':').Append(type.BaseType.ToFQDisplayString());
77+
78+
foreach (var iface in type.Interfaces.OrderBy(i => i.ToFQDisplayString()))
79+
sb.Append(',').Append(iface.ToFQDisplayString());
80+
81+
sb.Append('{');
82+
83+
// Include non-private, non-compiler-generated members
84+
foreach (var member in type.GetMembers()
85+
.Where(m => m.DeclaredAccessibility != Accessibility.Private && !m.IsImplicitlyDeclared)
86+
.OrderBy(m => m.Name)
87+
.ThenBy(m => m.Kind))
88+
{
89+
switch (member)
90+
{
91+
case IFieldSymbol f:
92+
sb.Append(f.DeclaredAccessibility).Append(' ');
93+
if (f.IsStatic) sb.Append("static ");
94+
sb.Append(f.Type.ToFQDisplayString()).Append(' ').Append(f.Name).Append(';');
95+
break;
96+
97+
case IPropertySymbol p:
98+
sb.Append(p.DeclaredAccessibility).Append(' ');
99+
if (p.IsStatic) sb.Append("static ");
100+
sb.Append(p.Type.ToFQDisplayString()).Append(' ').Append(p.Name);
101+
if (p.GetMethod != null) sb.Append("{get;}");
102+
if (p.SetMethod != null) sb.Append("{set;}");
103+
break;
104+
105+
case IMethodSymbol m when m.MethodKind == MethodKind.Ordinary:
106+
sb.Append(m.DeclaredAccessibility).Append(' ');
107+
if (m.IsStatic) sb.Append("static ");
108+
sb.Append(m.ReturnType.ToFQDisplayString()).Append(' ').Append(m.Name);
109+
sb.Append('(');
110+
sb.Append(string.Join(",", m.Parameters.Select(p => p.Type.ToFQDisplayString())));
111+
sb.Append(')').Append(';');
112+
break;
113+
114+
case INamedTypeSymbol nested:
115+
AppendType(sb, nested);
116+
break;
117+
}
118+
}
119+
120+
sb.Append('}');
18121
}
19122

20123
public int GetHashCode(Compilation obj) => obj.References.GetHashCode();

src/Controls/src/SourceGen/GeneratorHelpers.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,15 @@ public static string EscapeIdentifier(string identifier)
5555
}
5656
try
5757
{
58-
return new XamlProjectItemForIC(projectItem!, ParseXaml(text.ToString(), assemblyCaches));
58+
return new XamlProjectItemForIC(projectItem!, text.ToString());
5959
}
6060
catch (Exception e)
6161
{
6262
return new XamlProjectItemForIC(projectItem!, e);
6363
}
6464
}
6565

66-
static SGRootNode? ParseXaml(string xaml, AssemblyAttributes assemblyCaches)
66+
public static SGRootNode? ParseXaml(string xaml, AssemblyAttributes assemblyCaches)
6767
{
6868
List<string> warningDisableList = [];
6969
var nsmgr = new XmlNamespaceManager(new NameTable());

src/Controls/src/SourceGen/InitializeComponentCodeWriter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ PrePost newblock() =>
3333
codeWriter.WriteLine($"#pragma warning disable {xamlItem.ProjectItem.NoWarn}");
3434
codeWriter.WriteLine();
3535
}
36-
var root = xamlItem.Root!;
36+
var root = GeneratorHelpers.ParseXaml(xamlItem.Xaml!, xmlnsCache)!;
3737

3838
string accessModifier = "public";
3939
INamedTypeSymbol? rootType = null;
@@ -89,7 +89,7 @@ PrePost newblock() =>
8989
{
9090
var methodName = genSwitch ? "InitializeComponentSourceGen" : "InitializeComponent";
9191
codeWriter.WriteLine($"private partial void {methodName}()");
92-
xamlItem.Root!.XmlType.TryResolveTypeSymbol(null, compilation, xmlnsCache, typeCache, out var baseType);
92+
root!.XmlType.TryResolveTypeSymbol(null, compilation, xmlnsCache, typeCache, out var baseType);
9393
var sgcontext = new SourceGenContext(codeWriter, compilation, sourceProductionContext, xmlnsCache, typeCache, rootType!, baseType, xamlItem.ProjectItem);
9494
using (newblock())
9595
{

src/Controls/src/SourceGen/TrackingNames.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public class TrackingNames
77
{
88
public const string CssProjectItemProvider = nameof(CssProjectItemProvider);
99
public const string ProjectItemProvider = nameof(ProjectItemProvider);
10-
public const string ReferenceCompilationProvider = nameof(ReferenceCompilationProvider);
10+
public const string CompilationProvider = nameof(CompilationProvider);
1111
public const string ReferenceTypeCacheProvider = nameof(ReferenceTypeCacheProvider);
1212
public const string XmlnsDefinitionsProvider = nameof(XmlnsDefinitionsProvider);
1313
public const string XamlProjectItemProviderForCB = nameof(XamlProjectItemProviderForCB);

src/Controls/src/SourceGen/XamlGenerator.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext)
2828
// System.Diagnostics.Debugger.Launch();
2929
// }
3030
#endif
31-
// Only provide a new Compilation when the references change
31+
// Only provide a new Compilation when the references or syntax trees change
3232
var referenceCompilationProvider = initContext.CompilationProvider
33-
.WithComparer(new CompilationReferencesComparer())
34-
.WithTrackingName(TrackingNames.ReferenceCompilationProvider);
33+
.WithComparer(new CompilationSignaturesComparer())
34+
.WithTrackingName(TrackingNames.CompilationProvider);
3535

3636
var referenceTypeCacheProvider = referenceCompilationProvider
3737
.Select(GetTypeCache)
@@ -63,13 +63,13 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext)
6363
.WithTrackingName(TrackingNames.CssProjectItemProvider);
6464

6565
var xamlSourceProviderForCB = xamlProjectItemProviderForCB
66-
.Combine(xmlnsDefinitionsProvider, referenceTypeCacheProvider, referenceCompilationProvider)
66+
.Combine(xmlnsDefinitionsProvider, referenceTypeCacheProvider, initContext.CompilationProvider)
6767
.Select(GetSource)
6868
.WithTrackingName(TrackingNames.XamlSourceProviderForCB);
6969

7070
var compilationWithCodeBehindProvider = xamlSourceProviderForCB
7171
.Collect()
72-
.Combine(referenceCompilationProvider)
72+
.Combine(initContext.CompilationProvider)
7373
.Select(static (t, ct) =>
7474
{
7575
var compilation = t.Right;
@@ -515,7 +515,7 @@ static bool CanSourceGenXaml(XamlProjectItemForIC? xamlItem, Compilation compila
515515
var itemName = projItem.ManifestResourceName ?? projItem.RelativePath;
516516
if (itemName == null)
517517
return false;
518-
if (xamlItem.Root == null)
518+
if (xamlItem.Xaml == null)
519519
return false;
520520
return true;
521521
}

src/Controls/src/SourceGen/XamlProjectItemForIC.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ namespace Microsoft.Maui.Controls.SourceGen;
55

66
class XamlProjectItemForIC
77
{
8-
public XamlProjectItemForIC(ProjectItem projectItem, SGRootNode? root/*, XmlNamespaceManager nsmgr*/)
8+
public XamlProjectItemForIC(ProjectItem projectItem, string? xaml)
99
{
1010
ProjectItem = projectItem;
11-
Root = root;
12-
// Nsmgr = nsmgr;
11+
Xaml = xaml;
1312
}
1413

1514
public XamlProjectItemForIC(ProjectItem projectItem, Exception exception)
@@ -19,7 +18,6 @@ public XamlProjectItemForIC(ProjectItem projectItem, Exception exception)
1918
}
2019

2120
public ProjectItem ProjectItem { get; }
22-
public SGRootNode? Root { get; }
23-
// public XmlNamespaceManager? Nsmgr { get; }
21+
public string? Xaml { get; }
2422
public Exception? Exception { get; }
2523
}

0 commit comments

Comments
 (0)