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

Source generators/timing #55892

Merged
merged 12 commits into from
Sep 3, 2021

Conversation

chsienki
Copy link
Contributor

@chsienki chsienki commented Aug 25, 2021

Implements wall clock timing for the generator driver and each generator running within it.

Also implements a roslyn ETW event source that can be used to emit performance data. For now we're only using it for generators but we'll likely want to add other things as we go.

Fixes #54770

As we're emitting ETW you can view the data live coming out of visual studio. I've created a small app you can run that allows you to view the data in realtime: https://github.com/chsienki/GeneratorTracer

Screenshot

@@ -13,7 +13,9 @@ Microsoft.CodeAnalysis.GeneratorDriver.WithUpdatedParseOptions(Microsoft.CodeAna
Microsoft.CodeAnalysis.GeneratorDriverOptions
Microsoft.CodeAnalysis.GeneratorDriverOptions.GeneratorDriverOptions() -> void
Microsoft.CodeAnalysis.GeneratorDriverOptions.GeneratorDriverOptions(Microsoft.CodeAnalysis.IncrementalGeneratorOutputKind disabledOutputs) -> void
Microsoft.CodeAnalysis.GeneratorDriverRunResult.RunTime.get -> System.TimeSpan
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@333fred These are new public APIs, but nothing major. Can we just get consensus over email?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Spoke in the API review meeting and agreed to call them ElapsedTime

@chsienki chsienki marked this pull request as ready for review August 25, 2021 20:25
@chsienki chsienki requested a review from a team as a code owner August 25, 2021 20:25
@chsienki
Copy link
Contributor Author

cc @NTaylorMullen @jasonmalinowski @eerhardt

@@ -78,6 +79,8 @@ private GeneratorState(GeneratorInfo info, ImmutableArray<GeneratedSyntaxTree> p

internal Exception? Exception { get; }

internal TimeSpan RunTime { get; }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jkoritzinsky We probably want to add timing info to the individual steps as well, but we can figure that out when we merge your changes in.

@chsienki
Copy link
Contributor Author

@dotnet/roslyn-compiler for review please :)

@NTaylorMullen
Copy link
Contributor

Love the visualizations, the extension to view it in real time and the foresight to add an ETW event. 👏

Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

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

🎆 This is awesome!

public const EventKeywords Performance = (EventKeywords)1;
}

public class Tasks
Copy link
Member

Choose a reason for hiding this comment

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

Copy link

Choose a reason for hiding this comment

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

You're correct @eerhardt. In this case though it looks like tasks are actually more desirable than the current setup. Please correct me if I read this wrong since I only skimmed the code, but it looks like this patch currently:

  • adds a Timer to the state of the source generators
  • sends a completion event with the duration for a given generator
  • sends a completion event with the duration for the generator driver

I'd maybe look at changing this to use the tasks here by doing the following:

  • emit a GeneratorDriverStart/GeneratorDriverStop event pair using the driver task and op codes
  • emit a GeneratorStart/GeneratorStop event pair using the generator task and op codes
  • calculate the run time by diffing the timestamps of the events (the infrastructure should already do this by embedding the duration in the *Stop events)

What's currently written should be fine, but would subject the times in the events to any issues the timer has with waking up etc. The times are already being provided by the event infrastructure so you can take advantage of that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I only used them so I could distinguish them in the viewer linked above. Happy to remove them but it seemed like a useful concept. Especially if we convert to a pair of start / stop.

Copy link
Member

Choose a reason for hiding this comment

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

so I could distinguish them in the viewer linked above

Where are you distinguishing them in that viewer? Is it this code?

https://github.com/chsienki/GeneratorTracer/blob/f05dfa908fb7e78741ce2cb96e21cc3c77927609/GeneratorTracer/Program.cs#L30

If so, I think you can just rename the ReportGeneratorDriverRunTime method => GeneratorDriverRunTime and that will be the name of your Event. But this is getting into "beyond my EventSource knowledge" area.

Especially if we convert to a pair of start / stop.

Yeah, if you convert to start / stop, then the Tasks would be useful.

src/Compilers/Core/Portable/LoggingEventSource.cs Outdated Show resolved Hide resolved
src/Compilers/Core/Portable/LoggingEventSource.cs Outdated Show resolved Hide resolved
src/Compilers/Core/Portable/LoggingEventSource.cs Outdated Show resolved Hide resolved
Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

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

The EventSource changes LGTM.

Nice work! I think this is really going to help in diagnosing perf issues related to source generators.

@eerhardt
Copy link
Member

@chsienki @jaredpar - any thoughts on also backporting this to VS 2019 / Roslyn 3.x? It may be useful to have there for diagnostic purposes when customers are running into sluggish VS performance (e.g. dotnet/runtime#56702).

@chsienki
Copy link
Contributor Author

@chsienki @jaredpar - any thoughts on also backporting this to VS 2019 / Roslyn 3.x? It may be useful to have there for diagnostic purposes when customers are running into sluggish VS performance (e.g. dotnet/runtime#56702).

@eerhardt Sadly if we wanted to do that it'd have to be a manual port, as we've significantly altered the driver architecture between releases, so it seems unlikely.

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

The API shape looks good, I can't speak to the implementation or the EventSource stuff so somebody else needs to sign off on that.

src/Compilers/Core/Portable/CodeAnalysisEventSource.cs Outdated Show resolved Hide resolved
src/Compilers/Core/Portable/CodeAnalysisEventSource.cs Outdated Show resolved Hide resolved
internal void StartSingleGeneratorRunTime(string generatorName, string assemblyPath, string id) => WriteEvent(3, generatorName, assemblyPath, id);

[Event(4, Message = "Generator {0} ran for {2} ticks", Keywords = Keywords.Performance, Level = EventLevel.Informational, Opcode = EventOpcode.Stop, Task = Tasks.SingleGeneratorRunTime)]
internal void StopSingleGeneratorRunTime(string generatorName, string assemblyPath, long elapsedTicks, string id) => WriteEvent(4, generatorName, assemblyPath, elapsedTicks, id);
Copy link
Member

Choose a reason for hiding this comment

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

How does assemblyPath get used here given there is no {1} in the Message string?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's still available in the ETW event, just not directly in the message attached to it. See https://github.com/chsienki/GeneratorTracer/blob/master/GeneratorTracer/Program.cs#L35

@jasonmalinowski
Copy link
Member

@chsienki Hmm, one thought: what happens in the case of cancellation? It'd be good that if the IDE requests cancellation of a run, we still can see how much time was spent before we abandoned it. Practically a slow generator might get cancelled a lot in our case, and that's the actual indicator of the problem.

@chsienki
Copy link
Contributor Author

@chsienki Hmm, one thought: what happens in the case of cancellation? It'd be good that if the IDE requests cancellation of a run, we still can see how much time was spent before we abandoned it. Practically a slow generator might get cancelled a lot in our case, and that's the actual indicator of the problem.

@jasonmalinowski That's an interesting point. Currently we just let the OperationCancelledException propagate up to the caller, so there is no new instance to get any data/run results from. We could potentially catch it, and mark the driver state as having been cancelled. That way we'd retain whatever we recorded so far, but that seems like a bigger architectural change.

Given that, I think we should take this as-is for now, and work on the cancellation later

@jasonmalinowski
Copy link
Member

Given that, I think we should take this as-is for now, and work on the cancellation later

If this wasn't going to be a public API I'd heartily agree; in the current form though I'm not sure the IDE will get the benefit we want -- any telemetry we try reporting from would be skewed in a problematic way -- the long generators (that are regularly being cancelled) will be underrepresented in the data.

@jasonmalinowski jasonmalinowski dismissed their stale review August 31, 2021 17:53

This won't work for IDE needs, so we'll need something else.

@chsienki
Copy link
Contributor Author

@jasonmalinowski Ok, that makes sense to me.

@jaredpar Should we split out the public API and ETW parts into two parts? I wanted to get the ETW timing in ASAP, and don't want to delay if we need to redesign the public API parts.

@jasonmalinowski
Copy link
Member

@chsienki: we can make the API internal, right? The API wasn't being used by another layer I don't think in this?

@chsienki
Copy link
Contributor Author

@jasonmalinowski Yep, we can do that. I actually have a change that mostly enables the behaviour the IDE needs, although we need to go through API review again because it adds a WasCancelled property to run result.

@chsienki
Copy link
Contributor Author

Given the above, i'll make the API internal for now, and we can turn it back to public with the Cancellation tracking change afterwards.

@chsienki chsienki force-pushed the source-generators/timing branch from df25032 to 47a230e Compare August 31, 2021 22:08
@chsienki chsienki changed the base branch from main to release/dev17.0 September 1, 2021 22:44
[EventSource(Name = "Microsoft-CodeAnalysis-General")]
internal sealed class CodeAnalysisEventSource : EventSource
{
public static readonly CodeAnalysisEventSource Log = new CodeAnalysisEventSource();
Copy link
Member

@cston cston Sep 1, 2021

Choose a reason for hiding this comment

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

Log

Since we have a singleton instance, should CodeAnalysisEventSource have a private constructor? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, seems like a good idea.

@@ -155,10 +156,12 @@ static ImmutableArray<GeneratedSourceResult> getGeneratorSources(GeneratorState

internal GeneratorDriverState RunGeneratorsCore(Compilation compilation, DiagnosticBag? diagnosticsBag, CancellationToken cancellationToken = default)
{
using var timer = CodeAnalysisEventSource.Log.CreateGeneratorDriverRunTimer();
Copy link
Member

@cston cston Sep 1, 2021

Choose a reason for hiding this comment

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

using var timer

Consider moving after the if (_state.Generators.IsEmpty) { ... } case below, or use timer.Elapsed in that return value. #Closed

{
}

/// <summary>
/// Creates a new generator state that contains an exception and the associated diagnostic
/// </summary>
public GeneratorState(GeneratorInfo info, Exception e, Diagnostic error)
: this(info, ImmutableArray<GeneratedSyntaxTree>.Empty, ImmutableArray<ISyntaxInputNode>.Empty, ImmutableArray<IIncrementalGeneratorOutputNode>.Empty, ImmutableArray<GeneratedSyntaxTree>.Empty, ImmutableArray.Create(error), exception: e)
public GeneratorState(GeneratorInfo info, Exception e, Diagnostic error, TimeSpan runTime)
Copy link
Member

@cston cston Sep 1, 2021

Choose a reason for hiding this comment

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

runTime

Consider naming "elapsedTime" to match the field, here and in constructor below. #Closed

@chsienki chsienki force-pushed the source-generators/timing branch from 47a230e to 0d3943a Compare September 1, 2021 23:27
@chsienki
Copy link
Contributor Author

chsienki commented Sep 1, 2021

Addressed feedback + rebased to rel/dev17.0

{
private readonly SharedStopwatch _timer;
private readonly Action<TimeSpan> _callback;
private readonly bool _shouldExecuteCallback;
Copy link
Member

Choose a reason for hiding this comment

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

Rather than having a bool indicating whether to execute the callback, consider moving the check to the callback itself or set the callback to null or timeSpan => { } for the cases where we're not timing.

@@ -108,6 +115,11 @@ internal GeneratorRunResult(ISourceGenerator generator, ImmutableArray<Generated
/// collection will contain a single diagnostic indicating that the generator failed.
/// </remarks>
public Exception? Exception { get; }

/// <summary>
/// The wallclock time that elapsed while this generator was running.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// The wallclock time that elapsed while this generator was running.
/// The wall clock time that elapsed while this generator was running.

_callback = callback;
_shouldExecuteCallback = shouldExecuteCallback;

// start twice to improve accuracy. See AnalyzerExecutor::ExecuteAndCatchIfThrows for more details
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// start twice to improve accuracy. See AnalyzerExecutor::ExecuteAndCatchIfThrows for more details
// start twice to improve accuracy. See AnalyzerExecutor.ExecuteAndCatchIfThrows for more details

Copy link
Member

@cston cston left a comment

Choose a reason for hiding this comment

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

One suggestion to drop the bool RunTimer._shouldExecuteCallback field if possible, but not blocking.

@chsienki chsienki force-pushed the source-generators/timing branch from 0d3943a to 2bfa9cb Compare September 2, 2021 20:13
@chsienki chsienki enabled auto-merge (squash) September 2, 2021 20:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Roslyn should have an ETW/EventPipe source that we can use to report data
7 participants