-
-
Notifications
You must be signed in to change notification settings - Fork 155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve incremental source generator support #72
Comments
You'll want to use |
@Sergio0694 |
To address performance issues, register output as implementation source output, until #72 is addressed.
I'm implementing a couple of source generators myself and came across the same problem, that I was re-generating the source on any change within the same project. One way I found to drastically improve this is to use a comparer that will only evaluate Equals to false if the syntax nodes aren't equal (and ignore the compilation). Sample code is below... public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = GetIncrementalValuesProvider(context);
// Combine the CompilationProvider with the transformed data, but add a custom comparer so that
// differences can be intelligently determined in order to avoid unnecessarily invalidation of
// the cached generated code. This is vital to avoid lots of unneeded generation in the IDE, as
// without the comparer any change to any file in the same project will cause a re-generation.
// See performance tips at https://www.thinktecture.com/en/net/roslyn-source-generators-performance/
var combined = provider
.Combine(context.CompilationProvider)
.WithComparer(new SyntaxNodeComparer<ClassDeclarationSyntax>())
.Collect()
.SelectMany((array, _) => array.Distinct());
context.RegisterSourceOutput(combined, (ctx, data) => Execute(ctx, data.Item2, data.Item1));
}
private IncrementalValuesProvider<ClassDeclarationSyntax> GetIncrementalValuesProvider(
IncrementalGeneratorInitializationContext context) =>
context.SyntaxProvider.ForAttributeWithMetadataName(
AttributeType.FullName!,
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, _) => (ClassDeclarationSyntax)ctx.TargetNode); /// <summary>
/// Compares a syntax node and compilation combination by ignoring the compilation and only comparing the
/// syntax nodes for equality. This is used for comparing a syntax node returned from a syntax provider
/// with a combined compilation.
/// The reason we ignore the compilation in the comparision is because the syntax node is immutable, but the
/// compilation is not, and therefore if there are no changes to the syntax, <c>Equals</c> will evaluate to true.
/// If we included the compilation in the equality check also then we would always return false, which would
/// result in the generated source cache frequently being invalidated when there are no changes to the syntax.
/// </summary>
public class SyntaxNodeComparer<TSyntax> : IEqualityComparer<(TSyntax Node, Compilation Compilation)>
where TSyntax : SyntaxNode
{
public virtual bool Equals((TSyntax Node, Compilation Compilation) x, (TSyntax Node, Compilation Compilation) y)
{
return x.Node.Equals(y.Node);
}
public virtual int GetHashCode((TSyntax Node, Compilation Compilation) obj)
{
return obj.Node.GetHashCode();
}
} Note that I go this idea from https://www.thinktecture.com/en/net/roslyn-source-generators-performance/, which was a really great source of info for me. The issue with the solution above though is that the re-generation will only happen if the mapper file changes in some way, but not when one of the models change. In order to include the models in the comparison, you'd need to retrieve their syntax nodes and compare them too (which is more expensive... but probably a fair bit cheaper still than re-generating every time) I have found an issue in my own generation logic that I can't (easily) check the syntax of models that live in a different assembly. I know this is possible, but I haven't gotten around to working on it yet. I found that mapperly has the same limitation (i.e. if I update a model from a different project the mapperly generated file doesn't automatically re-generate). I think this is potentially a pretty common use-case for mappings (i.e. it's common for entities to live in a different project to DTO models), so would be great if this could be improved. |
Mapperly now uses Have you @nieldy encountered any performance issues while using Mapperly? |
@nieldy absolutely do NOT do this. It will root |
Ok, thanks for the tip @Sergio0694. I read about the I'm still quite confused about the effect of |
You just cannot do that. Gather all the info you need from the first transform step and make it equatable. No symbols or compilation objects or anything that's not equatable should get past the first transform step, generally speaking.
|
Ah right, that makes sense @Sergio0694, thanks. I think Regarding Rider behaviour vs VS. I just tried in VS and can see it is re-generating the source code when a model from a different assembly/project is modified, so it's clearly a quirk/bug in Rider. The generation also seems to be smoother and faster in VS (in Rider the code jumps about a little before it updates). Seems like JetBrains have a bit of work to do to get their implementation up to scratch. |
You don't have to wait, if that makes sense for you just use that. It'll just "get faster" when Roslyn starts supporting it 🙂 |
Looking at MVVM Toolkit it looks like it would be possible to convert Not sure how useful it is, this way |
Closing this for now, as @TimothyMakkison implemented great improvements by caching before normalizing whitespace which seems to work great. We'll probably revisit this later. |
I can think of two possible improvements, but both are a little tricky...
|
With the current approach, almost everything is regenerated all the time. This should be better abstracted to make better use of the incremental source generator. Approaches to be discussed.
The text was updated successfully, but these errors were encountered: