Skip to content
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

Make the STJ source generator cache friendly #86121

Merged
merged 10 commits into from
May 12, 2023

Conversation

eiriktsarpalis
Copy link
Member

This PR attempts to improve the cache friendliness of the incremental source generator by making the following changes:

  1. Removes all SourceProductionContext dependencies from the Parser class.
  2. Removes all Compilation and ISymbol dependencies from the Emitter class.
  3. Makes the intermediate SourceGenerationSpec model type immutable and structurally equatable so that it can be cached incrementally.

Contributes to #68353.

@eiriktsarpalis eiriktsarpalis self-assigned this May 11, 2023
@eiriktsarpalis eiriktsarpalis added this to the 8.0.0 milestone May 11, 2023
@eiriktsarpalis eiriktsarpalis requested review from layomia and krwq May 11, 2023 21:57
@ghost
Copy link

ghost commented May 11, 2023

Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis
See info in area-owners.md if you want to be subscribed.

Issue Details

This PR attempts to improve the cache friendliness of the incremental source generator by making the following changes:

  1. Removes all SourceProductionContext dependencies from the Parser class.
  2. Removes all Compilation and ISymbol dependencies from the Emitter class.
  3. Makes the intermediate SourceGenerationSpec model type immutable and structurally equatable so that it can be cached incrementally.

Contributes to #68353.

Author: eiriktsarpalis
Assignees: eiriktsarpalis
Labels:

area-System.Text.Json

Milestone: -

using System.Text.Json.Serialization;
using System.Text.Json.Reflection;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file has been moved under the Model folder, but git seems to think it's been deleted.

@@ -76,29 +76,23 @@ private sealed partial class Emitter
private const string JsonTypeInfoTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonTypeInfo";
private const string JsonTypeInfoResolverTypeRef = "global::System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver";

private static DiagnosticDescriptor TypeNotSupported { get; } = new DiagnosticDescriptor(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Descriptors moved to the Parser class.

string FormatNumber() => $"({type.FullyQualifiedName})({Convert.ToString(value, CultureInfo.InvariantCulture)})";
}

private static bool ShouldGenerateMetadata(TypeGenerationSpec typeSpec)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Methods from here on have been moved over from the TypeGenerationSpec class.

@@ -112,26 +111,41 @@ private sealed class Parser
// Needed for converter validation
private readonly Type? _jsonConverterOfTType;

// Keeps track of generated context type names
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved from the Emitter class.

var typeRef = new TypeRef(type);
typeInfoPropertyName ??= type.GetTypeInfoPropertyName();

if (classType is ClassType.TypeUnsupportedBySourceGen)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diagnostic reports moved over from the Emitter class.

@@ -1266,6 +1265,11 @@ bool MatchesConstructorParameter(ParameterGenerationSpec paramSpec)
out bool setterIsVirtual,
out bool setterIsInitOnly);

if (!isPublic && !memberType.IsPublic)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checks extracted from the callsite.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a bug fix?

}

[Fact]
public void CanGetAttributes()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test deleted since the source generator no longer surfaces "TypeWrapper" values. The reflection wrappers will eventually get removed in a follow-up PR.

@eiriktsarpalis eiriktsarpalis requested a review from tarekgh May 11, 2023 22:10
#else
public
#endif
enum JsonIgnoreCondition
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this changing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are part of the SG model which I made public for unit testing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's necessary for unit testing could we leave them as internal and use InternalsVisibleTo from the unit tests? It's probably not a huge deal, but exposing public surface area from the source generator does create an opportunity for someone to reference it and depend on these. We intend to not support that, but... yeah.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discussed this with @ericstj and @layomia and we figured that these wouldn't be public in practice since there's no easy way for users to reference them directly. The source generator classes themselves are public already but we don't particularly care about keeping them backward compatible. The SDK assemblies appear to also have public types for the same reason. I'd rather we fall back to IVT if there's strong pushback to the idea of making the model types public.

cc @ViktorHofer

Copy link
Member

@ViktorHofer ViktorHofer May 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong preference here just wanted to share that I noticed some issues with IVT in source generators. I.e. winforms heavily depends on IVT for their testing and when adding polyfills to netstandard2.0 like NullableAttributes.cs (which runtime does implicitly you get compiler errors. That's because the internal types are visible by the consumer which clash with the ones provided via the framework, i.e. when targeting .NETCoreApp.

Unsure if our source generator tests reference the source generator output assemblies directly though.

Co-authored-by: Stephen Toub <stoub@microsoft.com>
/// </summary>
[return: NotNullIfNotNull(nameof(location))]
public static Location? GetTrimmedLocation(this Location? location)
=> location is null ? null : Location.Create(location.SourceTree?.FilePath ?? "", location.SourceSpan, location.GetLineSpan().Span);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've now duplicated this in several of our generators. @chsienki, should Roslyn include helpers for this (and maybe more generally on a Diagnostic so that someone writing a source generator can easily avoid referencing the compilation from a Diagnostic they hold onto)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that seems like a good candidate for a new API

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you validated this has the desired impact on recomputation in VS?

eiriktsarpalis and others added 2 commits May 12, 2023 14:32
@eiriktsarpalis
Copy link
Member Author

Have you validated this has the desired impact on recomputation in VS?

Yes, both in unit testing and debugging VS itself. Curiously enough though it appears to be re-triggering periodically every ~20 seconds or so regardless of whether it is needed or not. @chsienki @sharwell is this expected behavior?

@eiriktsarpalis eiriktsarpalis merged commit c21406d into dotnet:main May 12, 2023
@eiriktsarpalis eiriktsarpalis deleted the fix/source-gen-caching branch May 12, 2023 17:48
{
GenerateTypeInfo(typeGenerationSpec);
_typeIndex.Add(spec.TypeRef, spec);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: _typeCache is a bit more readable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will address in a follow-up.

namespace System.Text.Json.SourceGeneration
{
internal enum ClassType
public enum ClassType
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we can remove the ctors part of line 9. Also this is tangential but at this point are there any types that we don't emit source for? I remember multi-dim arrays but we might have switched to generating metadata that throws NotSupportedException in some way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, and we should eventually. I wanted to scope this PR to refactoring only and avoid any changes to the underlying logic.


namespace System.Text.Json.SourceGeneration
{
/// <summary>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be used here to avoid duplication and future divergence?

#if DEBUG
MetadataLoadContext = _metadataLoadContext,
#endif
ContextGenerationSpecs = contextGenSpecList.ToImmutableEquatableArray(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: match the names

Suggested change
ContextGenerationSpecs = contextGenSpecList.ToImmutableEquatableArray(),
ContextGenerationSpecs = contextGenSpecs.ToImmutableEquatableArray(),

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix in a follow-up PR.

@@ -1266,6 +1265,11 @@ bool MatchesConstructorParameter(ParameterGenerationSpec paramSpec)
out bool setterIsVirtual,
out bool setterIsInitOnly);

if (!isPublic && !memberType.IsPublic)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a bug fix?

string FormatNumber() => $"({type.FullyQualifiedName})({Convert.ToString(value, CultureInfo.InvariantCulture)})";
}

private static bool ShouldGenerateMetadata(TypeGenerationSpec typeSpec)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the bool value be a property on TypeGenerationSpec?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm planning on doing this in a follow-up PR. Generally speaking the emitter should just emit and have no domain logic of its own. But I wanted to avoid adding new properties in the scope of this PR.

private static bool IsGenerationModeSpecified(TypeGenerationSpec typeSpec, JsonSourceGenerationMode mode)
=> typeSpec.GenerationMode == JsonSourceGenerationMode.Default || (mode & typeSpec.GenerationMode) != 0;

public static bool TryFilterSerializableProps(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed before that this should be precomputed by the parser...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and I'll move in a follow-up PR. For this change I only wanted to make the minimum number of rearrangements to make caching viable without making substantial changes to the core logic.

public class CompilationHelper
{
private static readonly CSharpParseOptions s_parseOptions =
new CSharpParseOptions(kind: SourceCodeKind.Regular, documentationMode: DocumentationMode.Parse);

#if ROSLYN4_0_OR_GREATER
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this PR provide an opportunity to switch to some helpers in RoslynTestUtils? https://github.com/dotnet/runtime/blob/main/src/libraries/Common/tests/SourceGenerators/RoslynTestUtils.cs

@chsienki
Copy link
Contributor

Have you validated this has the desired impact on recomputation in VS?

Yes, both in unit testing and debugging VS itself. Curiously enough though it appears to be re-triggering periodically every ~20 seconds or so regardless of whether it is needed or not. @chsienki @sharwell is this expected behavior?

When you say re-triggering, do you mean the whole generator is running, or just the inputs, and the generator finishes early because everything is cached?

@eiriktsarpalis
Copy link
Member Author

When you say re-triggering, do you mean the whole generator is running

The whole thing E2E.

@ghost ghost locked as resolved and limited conversation to collaborators Jun 12, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants