Skip to content

Make the STJ source generator cache friendly - #86121

Merged
eiriktsarpalis merged 10 commits into
dotnet:mainfrom
eiriktsarpalis:fix/source-gen-caching
May 12, 2023
Merged

eiriktsarpalis merged 10 commits into
dotnet:mainfrom
eiriktsarpalis:fix/source-gen-caching

Conversation

@eiriktsarpalis

Copy link
Copy Markdown
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 krwq and layomia May 11, 2023 21:57
@ghost

ghost commented May 11, 2023

Copy link
Copy Markdown

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
Copy Markdown
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.

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
Copy Markdown
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
Copy Markdown
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.

// Needed for converter validation
private readonly Type? _jsonConverterOfTType;

// Keeps track of generated context type names

Copy link
Copy Markdown
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
Copy Markdown
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.

out bool setterIsVirtual,
out bool setterIsInitOnly);

if (!isPublic && !memberType.IsPublic)

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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

@ViktorHofer ViktorHofer May 12, 2023 •

Copy link
Copy Markdown
Member

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
Copy Markdown
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
Copy Markdown
Member

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

@stephentoub stephentoub left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
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
foreach (TypeGenerationSpec spec in _currentContext.GeneratedTypes)
{
GenerateTypeInfo(typeGenerationSpec);
_typeIndex.Add(spec.TypeRef, spec);

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

out bool setterIsVirtual,
out bool setterIsInitOnly);

if (!isPublic && !memberType.IsPublic)

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

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

#if ROSLYN4_0_OR_GREATER

Copy link
Copy Markdown
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
Copy Markdown
Member

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
Copy Markdown
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