Skip to content

Commit f07bb20

Browse files
committed
Optimize codegen when hooks are not used
1 parent 2542767 commit f07bb20

File tree

2 files changed

+61
-13
lines changed

2 files changed

+61
-13
lines changed

src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
1717
/// <param name="NotifiedCommandNames">The sequence of commands to notify.</param>
1818
/// <param name="NotifyPropertyChangedRecipients">Whether or not the generated property also broadcasts changes.</param>
1919
/// <param name="NotifyDataErrorInfo">Whether or not the generated property also validates its value.</param>
20+
/// <param name="IsOldPropertyValueDirectlyReferenced">Whether the old property value is being directly referenced.</param>
2021
/// <param name="ForwardedAttributes">The sequence of forwarded attributes for the generated property.</param>
2122
internal sealed record PropertyInfo(
2223
string TypeNameWithNullabilityAnnotations,
@@ -27,4 +28,5 @@ internal sealed record PropertyInfo(
2728
EquatableArray<string> NotifiedCommandNames,
2829
bool NotifyPropertyChangedRecipients,
2930
bool NotifyDataErrorInfo,
31+
bool IsOldPropertyValueDirectlyReferenced,
3032
EquatableArray<AttributeInfo> ForwardedAttributes);

src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ public static bool TryGetInfo(
111111
bool hasOrInheritsClassLevelNotifyPropertyChangedRecipients = false;
112112
bool hasOrInheritsClassLevelNotifyDataErrorInfo = false;
113113
bool hasAnyValidationAttributes = false;
114+
bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(fieldSymbol, propertyName);
114115

115116
// Track the property changing event for the property, if the type supports it
116117
if (shouldInvokeOnPropertyChanging)
@@ -263,6 +264,7 @@ public static bool TryGetInfo(
263264
notifiedCommandNames.ToImmutable(),
264265
notifyRecipients,
265266
notifyDataErrorInfo,
267+
isOldPropertyValueDirectlyReferenced,
266268
forwardedAttributes.ToImmutable());
267269

268270
diagnostics = builder.ToImmutable();
@@ -637,6 +639,38 @@ private static bool TryGetNotifyDataErrorInfo(
637639
return false;
638640
}
639641

642+
/// <summary>
643+
/// Checks whether the generated code has to directly reference the old property value.
644+
/// </summary>
645+
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
646+
/// <param name="propertyName">The name of the property being generated.</param>
647+
/// <returns>Whether the generated code needs direct access to the old property value.</returns>
648+
private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbol, string propertyName)
649+
{
650+
// Check On<PROPERTY_NAME>Changing(<PROPERTY_TYPE> oldValue, <PROPERTY_TYPE> newValue) first
651+
foreach (ISymbol symbol in fieldSymbol.ContainingType.GetMembers($"On{propertyName}Changing"))
652+
{
653+
// No need to be too specific as we're not expecting false positives (which also wouldn't really
654+
// cause any problems anyway, just produce slightly worse codegen). Just checking the number of
655+
// parameters is good enough, and keeps the code very simple and cheap to run.
656+
if (symbol is IMethodSymbol { Parameters.Length: 2 })
657+
{
658+
return true;
659+
}
660+
}
661+
662+
// Do the same for On<PROPERTY_NAME>Changed(<PROPERTY_TYPE> oldValue, <PROPERTY_TYPE> newValue)
663+
foreach (ISymbol symbol in fieldSymbol.ContainingType.GetMembers($"On{propertyName}Changed"))
664+
{
665+
if (symbol is IMethodSymbol { Parameters.Length: 2 })
666+
{
667+
return true;
668+
}
669+
}
670+
671+
return false;
672+
}
673+
640674
/// <summary>
641675
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
642676
/// </summary>
@@ -683,15 +717,18 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
683717
string name => IdentifierName(name)
684718
};
685719

686-
// Store the old value for later. This code generates a statement as follows:
687-
//
688-
// <PROPERTY_TYPE> __oldValue = <FIELD_EXPRESSIONS>;
689-
setterStatements.Add(
690-
LocalDeclarationStatement(
691-
VariableDeclaration(propertyType)
692-
.AddVariables(
693-
VariableDeclarator(Identifier("__oldValue"))
694-
.WithInitializer(EqualsValueClause(fieldExpression)))));
720+
if (propertyInfo.NotifyPropertyChangedRecipients || propertyInfo.IsOldPropertyValueDirectlyReferenced)
721+
{
722+
// Store the old value for later. This code generates a statement as follows:
723+
//
724+
// <PROPERTY_TYPE> __oldValue = <FIELD_EXPRESSIONS>;
725+
setterStatements.Add(
726+
LocalDeclarationStatement(
727+
VariableDeclaration(propertyType)
728+
.AddVariables(
729+
VariableDeclarator(Identifier("__oldValue"))
730+
.WithInitializer(EqualsValueClause(fieldExpression)))));
731+
}
695732

696733
// Add the OnPropertyChanging() call first:
697734
//
@@ -701,13 +738,22 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
701738
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changing"))
702739
.AddArgumentListArguments(Argument(IdentifierName("value")))));
703740

741+
// Optimization: if the previous property value is not being referenced (which we can check by looking for an existing
742+
// symbol matching the name of either of these generated methods), we can pass a default expression and avoid generating
743+
// a field read, which won't otherwise be elided by Roslyn. Otherwise, we just store the value in a local as usual.
744+
ArgumentSyntax oldPropertyValueArgument = propertyInfo.IsOldPropertyValueDirectlyReferenced switch
745+
{
746+
true => Argument(IdentifierName("__oldValue")),
747+
false => Argument(LiteralExpression(SyntaxKind.DefaultLiteralExpression, Token(SyntaxKind.DefaultKeyword)))
748+
};
749+
704750
// Also call the overload after that:
705751
//
706-
// On<PROPERTY_NAME>Changing(__oldValue, value);
752+
// On<PROPERTY_NAME>Changing(<OLD_PROPERTY_VALUE_EXPRESSION>, value);
707753
setterStatements.Add(
708754
ExpressionStatement(
709755
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changing"))
710-
.AddArgumentListArguments(Argument(IdentifierName("__oldValue")), Argument(IdentifierName("value")))));
756+
.AddArgumentListArguments(oldPropertyValueArgument, Argument(IdentifierName("value")))));
711757

712758
// Gather the statements to notify dependent properties
713759
foreach (string propertyName in propertyInfo.PropertyChangingNames)
@@ -757,11 +803,11 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
757803

758804
// Do the same for the overload, as above:
759805
//
760-
// On<PROPERTY_NAME>Changed(__oldValue, value);
806+
// On<PROPERTY_NAME>Changed(<OLD_PROPERTY_VALUE_EXPRESSION>, value);
761807
setterStatements.Add(
762808
ExpressionStatement(
763809
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changed"))
764-
.AddArgumentListArguments(Argument(IdentifierName("__oldValue")), Argument(IdentifierName("value")))));
810+
.AddArgumentListArguments(oldPropertyValueArgument, Argument(IdentifierName("value")))));
765811

766812
// Gather the statements to notify dependent properties
767813
foreach (string propertyName in propertyInfo.PropertyChangedNames)

0 commit comments

Comments
 (0)