-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Conversation
@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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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();
src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs
Outdated
Show resolved
Hide resolved
Azure Pipelines successfully started running 4 pipeline(s). |
} | ||
|
||
/// <summary> | ||
/// The Generator that was running during the recorded time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// <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; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.
src/Compilers/Core/Portable/SourceGeneration/GeneratorTimerExtensions.cs
Outdated
Show resolved
Hide resolved
{ | ||
if (kvp.Key != node) | ||
{ | ||
result = result.Subtract(kvp.Value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.
src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Semantic/SourceGeneration/SyntaxAwareGeneratorTests.cs
Outdated
Show resolved
Hide resolved
/azp run |
Azure Pipelines successfully started running 4 pipeline(s). |
@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)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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.
ab190e4
to
9a101dc
Compare
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 ```
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 ```
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 ```
* 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
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