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

Implement source generator timing APIs #60267

Merged
merged 6 commits into from
May 2, 2022

Conversation

chsienki
Copy link
Contributor

Implements timing APIs as approved here: #59398

Also fixes a long standing problem where syntax time was misattributed to whichever generator first updated its syntax nodes. This was less of a problem when it was only ETW events, but now we're exposing it via API it needs to be correct.

Closes #59398

@chsienki
Copy link
Contributor Author

@dotnet/roslyn-compiler for review please

@@ -51,6 +53,8 @@ public IStateTable GetSyntaxInputTable(SyntaxInputNode syntaxInputNode, NodeStat
// when we don't have a value for this node, we update all the syntax inputs at once
if (!_tableBuilder.Contains(syntaxInputNode))
{
_causedUpdate = syntaxInputNode;
Copy link
Member

Choose a reason for hiding this comment

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

For generators that don't consume syntax at all, they'll still be updated or do we need other methods updated too? A generator that consumes nothing but is still stupidly expensive will get flagged? This is a question from the perspective of me not quite knowing the implementation here of this type or what it's for.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this offset calculation only affects generators that touch syntax. Basically first generator to hit a syntax input updates all the others. That means the wall clock time calculated in GeneratorDriver here https://github.com/dotnet/roslyn/pull/60267/files#diff-d19793306af7a0ed1d42db723b6eba037523b35a290799c83402e854d7e5917dR261 incorrectly includes syntax timings for other generators.

Regular generators that don't use syntax will just have an offset adjustment of 0. The 'victim' that ran all the updates gets a negative offset to remove the extra time. Everything else gets the time spend calculating its nodes to ensure its accurate.

Assert.NotEqual(TimeSpan.Zero, timing.ElapsedTime);
Assert.Equal(2, timing.GeneratorTimes.Length);

// check generator one took at least 'sleepTimeInMs'
Copy link
Member

Choose a reason for hiding this comment

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

Is there a guarantee that Thread.Sleep(50) ensures that underlying timer will advance far enough? I'm always paranoid with mixing different types of timers. The Win32 Sleep call at least:

The system clock "ticks" at a constant rate. If dwMilliseconds is less than the resolution of the system clock, the thread may sleep for less than the specified length of time. If dwMilliseconds is greater than one tick but less than two, the wait can be anywhere between one and two ticks, and so on.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I agonized over this as I really don't like relying on timing information, but couldn't really think of a better way to test it. Anecdotally 50ms seemed a good balance between not slowing down tests too much, but always passing.

Copy link
Member

Choose a reason for hiding this comment

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

Did you consider the following pattern:

using var mre = new ManualResetEvent(initialState: false);
...
var testGenerator = new PipelineCallbackGenerator(ctx =>
            {
                ctx.RegisterSourceOutput(ctx.SyntaxProvider.CreateSyntaxProvider<object>((s, _) => s is AssignmentExpressionSyntax, (c, _) => { mre.WaitOne(); return true; }), (spc, s) => { });
            }).AsSourceGenerator();
  
...
while (!<did timer elapse fare enough>)
{
  Thread.Sleep(1);
}
mre.Set();

@azure-pipelines
Copy link

Azure Pipelines successfully started running 4 pipeline(s).

}

/// <summary>
/// The Generator that was running during the recorded time.
Copy link
Member

@cston cston Apr 6, 2022

Choose a reason for hiding this comment

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

Generator

Consider using "generator" (lowercase) for consistency with other comments, or use <see cref="Generator"/>. #Closed

/// <remarks>
/// This can be more than the sum of times in <see cref="GeneratorTimes"/> as it includes other costs such as setup.
/// </remarks>
public TimeSpan ElapsedTime { get; }
Copy link
Member

Choose a reason for hiding this comment

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

{ get; }

In the API proposal, these were fields rather than properties. Is the change expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it should have always been properties to match the other *Result[s] structs. I'll run it by API review in email to double check before merging though.

{
if (kvp.Key != node)
{
result = result.Subtract(kvp.Value);
Copy link
Member

Choose a reason for hiding this comment

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

Subtract

Could we have subtracted off these times when calculating the time for _causedUpdate originally in GetSyntaxInputTable()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ooh, that's a really nice way to do it. Done.

@chsienki
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 4 pipeline(s).

@chsienki
Copy link
Contributor Author

@dotnet/roslyn-compiler for reviews please

foreach (var node in inputNodes)
{
// only add if this node ran at all during this pass
if (_syntaxTimes.ContainsKey(node))
Copy link
Member

@cston cston Apr 25, 2022

Choose a reason for hiding this comment

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

_syntaxTimes.ContainsKey(node)

Consider using _syntaxTimes.TryGetValue(out var value) to avoid two lookups. #Resolved

var elapsed = sw.Elapsed;

// if this node isn't the one that caused the update, ensure we remember it and remove the time it took from the requester
if (syntaxInputBuilders[i].node != syntaxInputNode)
Copy link
Member

@cston cston Apr 25, 2022

Choose a reason for hiding this comment

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

syntaxInputBuilders[i].node

Consider extracting a local. (If the local is created at the start of the for (...) body, it can be used in the catch below as well.) #Closed

// if this node isn't the one that caused the update, ensure we remember it and remove the time it took from the requester
if (syntaxInputBuilders[i].node != syntaxInputNode)
{
_syntaxTimes[syntaxInputNode] = _syntaxTimes[syntaxInputNode].Subtract(elapsed);
Copy link
Member

@cston cston Apr 25, 2022

Choose a reason for hiding this comment

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

_syntaxTimes[syntaxInputNode].Subtract(elapsed)

We're subtracting from _syntaxTimes[syntaxInputNode] but where did we add time for that node? #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.

It gets set to TimeSpan.Zero on line 73. This is the node that actually asked for the update, and so got unfairly attributed all the time of the other nodes. We remove the other nodes time from zero, so its offset is actually negative (the sum of all the time we recorded for it that was actually processing other nodes) so that it gets its time reduced and every other node gets its time increased.

Copy link
Member

Choose a reason for hiding this comment

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

so its offset is actually negative

Does that mean _syntaxTimes[i] is actually an offset rather than the elapsed time for node i? If so, where is the elapsed time calculated? (Sorry if I'm misunderstanding.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, its an offset that gets applied to calculate the correct elapsed time. We do the actual timing in the generator driver here:

using var generatorTimer = CodeAnalysisEventSource.Log.CreateSingleGeneratorRunTimer(state.Generators[i], (t) => t.Add(syntaxStoreBuilder.GetRuntimeAdjustment(stateBuilder[i].InputNodes)));

That sits around the entire graph calculation operation for a given generator, but because we do the syntax nodes in parallel whenever the first one is requested, we have to adjust via the offset to ensure we actually have the correct times for each generator, and we don't unfairly claim that one generator was responsible for all the time taken to calculate syntax nodes, and the others took no time.

@RikkiGibson RikkiGibson self-assigned this Apr 29, 2022
@chsienki chsienki force-pushed the source-generators/timing-apis branch from ab190e4 to 9a101dc Compare May 2, 2022 18:55
@chsienki chsienki enabled auto-merge (squash) May 2, 2022 20:06
@chsienki chsienki merged commit 80814bf into dotnet:main May 2, 2022
@ghost ghost added this to the Next milestone May 2, 2022
@Cosifne Cosifne modified the milestones: Next, 17.3 P2 May 31, 2022
jaredpar added a commit to jaredpar/roslyn that referenced this pull request Jun 2, 2022
This integrates the source generator timing work, dotnet#60267, into the
`/reportAnalyzer` framework. A sample of the new output from a simple
`net7.0` console app is as follows:

```
Microsoft (R) Visual C# Compiler version 4.3.0-dev (<developer build>)
Copyright (C) Microsoft Corporation. All rights reserved.

warning CS2023: Ignoring /noconfig option because it was specified in a response file
NOTE: Elapsed time may be less than analyzer execution time because analyzers can run concurrently.

Total analyzer execution time: 0.303 seconds.

Time (s)    %   Analyzer
   0.248   81   Microsoft.CodeAnalysis.CSharp.NetAnalyzers, Version=7.0.6.21701, Culture=neutral, PublicKeyToken=31bf3856ad364e35
   0.246   81      Microsoft.NetCore.CSharp.Analyzers.Runtime.CSharpDetectPreviewFeatureAnalyzer (CA2252)
   // trimmed output
  <0.001   <1      Microsoft.NetFramework.CSharp.Analyzers.CSharpSpecifyMessageBoxOptionsAnalyzer ()

   0.055   18   Microsoft.CodeAnalysis.NetAnalyzers, Version=7.0.6.21701, Culture=neutral, PublicKeyToken=31bf3856ad364e35
   0.034   11      Microsoft.NetCore.Analyzers.InteropServices.PlatformCompatibilityAnalyzer (CA1416)
   // trimmed output
  <0.001   <1      Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines.TypeNamesShouldNotMatchNamespacesAnalyzer (CA1724)

Total generator execution time: 0.088 seconds.

Time (s)    %   Generators
   0.054   60   System.Text.Json.SourceGeneration, Version=7.0.6.22904, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
   0.054   60      System.Text.Json.SourceGeneration.JsonSourceGenerator
   0.016   18   System.Text.RegularExpressions.Generator, Version=7.0.6.22904, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
   0.016   18      System.Text.RegularExpressions.Generator.RegexGenerator
```
jaredpar added a commit to jaredpar/roslyn that referenced this pull request Jun 21, 2022
This integrates the source generator timing work, dotnet#60267, into the
`/reportAnalyzer` framework. A sample of the new output from a simple
`net7.0` console app is as follows:

```
Microsoft (R) Visual C# Compiler version 4.3.0-dev (<developer build>)
Copyright (C) Microsoft Corporation. All rights reserved.

warning CS2023: Ignoring /noconfig option because it was specified in a response file
NOTE: Elapsed time may be less than analyzer execution time because analyzers can run concurrently.

Total analyzer execution time: 0.303 seconds.

Time (s)    %   Analyzer
   0.248   81   Microsoft.CodeAnalysis.CSharp.NetAnalyzers, Version=7.0.6.21701, Culture=neutral, PublicKeyToken=31bf3856ad364e35
   0.246   81      Microsoft.NetCore.CSharp.Analyzers.Runtime.CSharpDetectPreviewFeatureAnalyzer (CA2252)
   // trimmed output
  <0.001   <1      Microsoft.NetFramework.CSharp.Analyzers.CSharpSpecifyMessageBoxOptionsAnalyzer ()

   0.055   18   Microsoft.CodeAnalysis.NetAnalyzers, Version=7.0.6.21701, Culture=neutral, PublicKeyToken=31bf3856ad364e35
   0.034   11      Microsoft.NetCore.Analyzers.InteropServices.PlatformCompatibilityAnalyzer (CA1416)
   // trimmed output
  <0.001   <1      Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines.TypeNamesShouldNotMatchNamespacesAnalyzer (CA1724)

Total generator execution time: 0.088 seconds.

Time (s)    %   Generators
   0.054   60   System.Text.Json.SourceGeneration, Version=7.0.6.22904, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
   0.054   60      System.Text.Json.SourceGeneration.JsonSourceGenerator
   0.016   18   System.Text.RegularExpressions.Generator, Version=7.0.6.22904, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
   0.016   18      System.Text.RegularExpressions.Generator.RegexGenerator
```
jaredpar added a commit to jaredpar/roslyn that referenced this pull request Jul 7, 2022
This integrates the source generator timing work, dotnet#60267, into the
`/reportAnalyzer` framework. A sample of the new output from a simple
`net7.0` console app is as follows:

```
Microsoft (R) Visual C# Compiler version 4.3.0-dev (<developer build>)
Copyright (C) Microsoft Corporation. All rights reserved.

warning CS2023: Ignoring /noconfig option because it was specified in a response file
NOTE: Elapsed time may be less than analyzer execution time because analyzers can run concurrently.

Total analyzer execution time: 0.303 seconds.

Time (s)    %   Analyzer
   0.248   81   Microsoft.CodeAnalysis.CSharp.NetAnalyzers, Version=7.0.6.21701, Culture=neutral, PublicKeyToken=31bf3856ad364e35
   0.246   81      Microsoft.NetCore.CSharp.Analyzers.Runtime.CSharpDetectPreviewFeatureAnalyzer (CA2252)
   // trimmed output
  <0.001   <1      Microsoft.NetFramework.CSharp.Analyzers.CSharpSpecifyMessageBoxOptionsAnalyzer ()

   0.055   18   Microsoft.CodeAnalysis.NetAnalyzers, Version=7.0.6.21701, Culture=neutral, PublicKeyToken=31bf3856ad364e35
   0.034   11      Microsoft.NetCore.Analyzers.InteropServices.PlatformCompatibilityAnalyzer (CA1416)
   // trimmed output
  <0.001   <1      Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines.TypeNamesShouldNotMatchNamespacesAnalyzer (CA1724)

Total generator execution time: 0.088 seconds.

Time (s)    %   Generators
   0.054   60   System.Text.Json.SourceGeneration, Version=7.0.6.22904, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
   0.054   60      System.Text.Json.SourceGeneration.JsonSourceGenerator
   0.016   18   System.Text.RegularExpressions.Generator, Version=7.0.6.22904, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
   0.016   18      System.Text.RegularExpressions.Generator.RegexGenerator
```
jaredpar added a commit that referenced this pull request Jul 7, 2022
* Integrate generator times into /reportAnalyzer

This integrates the source generator timing work, #60267, into the
`/reportAnalyzer` framework. A sample of the new output from a simple
`net7.0` console app is as follows:

```
Microsoft (R) Visual C# Compiler version 4.3.0-dev (<developer build>)
Copyright (C) Microsoft Corporation. All rights reserved.

warning CS2023: Ignoring /noconfig option because it was specified in a response file
NOTE: Elapsed time may be less than analyzer execution time because analyzers can run concurrently.

Total analyzer execution time: 0.303 seconds.

Time (s)    %   Analyzer
   0.248   81   Microsoft.CodeAnalysis.CSharp.NetAnalyzers, Version=7.0.6.21701, Culture=neutral, PublicKeyToken=31bf3856ad364e35
   0.246   81      Microsoft.NetCore.CSharp.Analyzers.Runtime.CSharpDetectPreviewFeatureAnalyzer (CA2252)
   // trimmed output
  <0.001   <1      Microsoft.NetFramework.CSharp.Analyzers.CSharpSpecifyMessageBoxOptionsAnalyzer ()

   0.055   18   Microsoft.CodeAnalysis.NetAnalyzers, Version=7.0.6.21701, Culture=neutral, PublicKeyToken=31bf3856ad364e35
   0.034   11      Microsoft.NetCore.Analyzers.InteropServices.PlatformCompatibilityAnalyzer (CA1416)
   // trimmed output
  <0.001   <1      Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines.TypeNamesShouldNotMatchNamespacesAnalyzer (CA1724)

Total generator execution time: 0.088 seconds.

Time (s)    %   Generators
   0.054   60   System.Text.Json.SourceGeneration, Version=7.0.6.22904, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
   0.054   60      System.Text.Json.SourceGeneration.JsonSourceGenerator
   0.016   18   System.Text.RegularExpressions.Generator, Version=7.0.6.22904, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
   0.016   18      System.Text.RegularExpressions.Generator.RegexGenerator
```

* PR feedback

* PR feedback

* PR feedback

* Test failure

* Fixed shared project file error

The missing .projitems entries in the .shproj was causing solution
explorer to not display the files in the shared projects.

The SLN file changes are just VS reacting to the updates to the shared
project files.

* Fixup tests
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.

Source Generators: Timing APIs
7 participants