-
Notifications
You must be signed in to change notification settings - Fork 40
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
Nuqleon.Linq.Expressions.Optimizers causes IDE0001 diagnostic to run incredibly slowly #138
Comments
* Remove netcoreapp3.1 as target framework .NET Core 3.1 has been out of support for a while now, and its continued presence is starting to cause spurious diagnostic messages, so we're removing it. * Update dotnet-format usage This tool has been built into the .NET SDK since 6.0, so the install step was actively unhelpful: it installed an older version. Also, the --check option no longer exists, and we use --verify-no-changes instead. Also, fix a couple of errors it now picks up on * Fix dotnet format command to pick up .NET SDK version * Perform restore of dotnet format separately dotnet/format#1892 suggests this as a workaround for the tool hanging * Fix restore command * Try fixing restore again * Disable dotnet-format step because of #138 * Reinstate dotnet-format but with exclusion * Disable the Job --------- Co-authored-by: Howard van Rooijen <[email protected]>
I've done a bit of experimentation to try to isolate the problem. I've been able to reproduce the issue with a project with just a couple of files. If we create a new C# console app, and add a reference to the <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nuqleon.Linq.Expressions.Optimizers" Version="1.0.0-beta.24" />
</ItemGroup>
</Project> and then add this file: using System.Linq.Expressions;
namespace TryToMakeIde0001GoSlowly;
internal class LotsOfExpressionTreesInInitializers
{
public static MemberTable DateTime { get; } = new MemberTable
{
(long ticks) => new global::System.DateTime(ticks),
(long ticks, DateTimeKind kind) => new global::System.DateTime(ticks, kind),
(int year, int month, int day) => new global::System.DateTime(year, month, day),
(int year, int month, int day, int hour, int minute, int second) => new global::System.DateTime(year, month, day, hour, minute, second),
(int year, int month, int day, int hour, int minute, int second, DateTimeKind kind) => new global::System.DateTime(year, month, day, hour, minute, second, kind),
(int year, int month, int day, int hour, int minute, int second, int millisecond) => new global::System.DateTime(year, month, day, hour, minute, second, millisecond),
(int year, int month, int day, int hour, int minute, int second, int millisecond, DateTimeKind kind) => new global::System.DateTime(year, month, day, hour, minute, second, millisecond, kind),
() => global::System.DateTime.MaxValue,
() => global::System.DateTime.MinValue,
(global::System.DateTime dt) => dt.Date,
(global::System.DateTime dt) => dt.Day,
(global::System.DateTime dt) => dt.DayOfWeek,
(global::System.DateTime dt) => dt.DayOfYear,
(global::System.DateTime dt) => dt.Hour,
(global::System.DateTime dt) => dt.Kind,
(global::System.DateTime dt) => dt.Millisecond,
(global::System.DateTime dt) => dt.Minute,
(global::System.DateTime dt) => dt.Month,
(global::System.DateTime dt) => dt.Second,
(global::System.DateTime dt) => dt.Ticks,
(global::System.DateTime dt) => dt.TimeOfDay,
(global::System.DateTime dt) => dt.Year,
(global::System.DateTime t1, global::System.DateTime t2) => global::System.DateTime.Compare(t1, t2),
(global::System.DateTime t1, global::System.DateTime t2) => global::System.DateTime.Equals(t1, t2),
(double d) => global::System.DateTime.FromOADate(d),
(long fileTime) => global::System.DateTime.FromFileTimeUtc(fileTime),
(int year) => global::System.DateTime.IsLeapYear(year),
(int year, int month) => global::System.DateTime.DaysInMonth(year, month),
(global::System.DateTime value, global::System.DateTimeKind kind) => global::System.DateTime.SpecifyKind(value, kind),
(global::System.DateTime dt) => dt.ToOADate(),
(global::System.DateTime dt, global::System.TimeSpan value) => dt.Add(value),
(global::System.DateTime dt, double value) => dt.AddDays(value),
(global::System.DateTime dt, double value) => dt.AddHours(value),
(global::System.DateTime dt, double value) => dt.AddMilliseconds(value),
(global::System.DateTime dt, double value) => dt.AddMinutes(value),
(global::System.DateTime dt, int months) => dt.AddMonths(months),
(global::System.DateTime dt, double value) => dt.AddSeconds(value),
(global::System.DateTime dt, long value) => dt.AddTicks(value),
(global::System.DateTime dt, int value) => dt.AddYears(value),
(global::System.DateTime dt, global::System.DateTime value) => dt.Subtract(value),
(global::System.DateTime dt, global::System.TimeSpan value) => dt.Subtract(value),
(global::System.DateTime dt, global::System.DateTime value) => dt.CompareTo(value),
(global::System.DateTime dt, global::System.DateTime value) => dt.Equals(value),
(global::System.DateTime dt, global::System.TimeSpan ts) => dt + ts,
(global::System.DateTime dt, global::System.TimeSpan ts) => dt - ts,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 - dt2,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 == dt2,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 != dt2,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 < dt2,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 <= dt2,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 > dt2,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 >= dt2,
}.ToReadOnly();
} it takes about 1 minute and 30 seconds before it greys out all those If you copy text from the list of lambdas in this file to the clipboard, VS goes unresponsive for over a minute too. I was wondering if this might be something specific to property initializers, so I tried putting the same collection initializer expression inside an ordinary method: using System.Linq.Expressions;
namespace TryToMakeIde0001GoSlowly;
internal class LotsOfExpressionTreesInInitializers
{
public static void UseInitializer()
{
var x = new MemberTable
{
(long ticks) => new global::System.DateTime(ticks),
(long ticks, DateTimeKind kind) => new global::System.DateTime(ticks, kind),
(int year, int month, int day) => new global::System.DateTime(year, month, day),
(int year, int month, int day, int hour, int minute, int second) => new global::System.DateTime(year, month, day, hour, minute, second),
(int year, int month, int day, int hour, int minute, int second, DateTimeKind kind) => new global::System.DateTime(year, month, day, hour, minute, second, kind),
(int year, int month, int day, int hour, int minute, int second, int millisecond) => new global::System.DateTime(year, month, day, hour, minute, second, millisecond),
(int year, int month, int day, int hour, int minute, int second, int millisecond, DateTimeKind kind) => new global::System.DateTime(year, month, day, hour, minute, second, millisecond, kind),
() => global::System.DateTime.MaxValue,
() => global::System.DateTime.MinValue,
(global::System.DateTime dt) => dt.Date,
(global::System.DateTime dt) => dt.Day,
(global::System.DateTime dt) => dt.DayOfWeek,
(global::System.DateTime dt) => dt.DayOfYear,
(global::System.DateTime dt) => dt.Hour,
(global::System.DateTime dt) => dt.Kind,
(global::System.DateTime dt) => dt.Millisecond,
(global::System.DateTime dt) => dt.Minute,
(global::System.DateTime dt) => dt.Month,
(global::System.DateTime dt) => dt.Second,
(global::System.DateTime dt) => dt.Ticks,
(global::System.DateTime dt) => dt.TimeOfDay,
(global::System.DateTime dt) => dt.Year,
(global::System.DateTime t1, global::System.DateTime t2) => global::System.DateTime.Compare(t1, t2),
(global::System.DateTime t1, global::System.DateTime t2) => global::System.DateTime.Equals(t1, t2),
(double d) => global::System.DateTime.FromOADate(d),
(long fileTime) => global::System.DateTime.FromFileTimeUtc(fileTime),
(int year) => global::System.DateTime.IsLeapYear(year),
(int year, int month) => global::System.DateTime.DaysInMonth(year, month),
(global::System.DateTime value, global::System.DateTimeKind kind) => global::System.DateTime.SpecifyKind(value, kind),
(global::System.DateTime dt) => dt.ToOADate(),
(global::System.DateTime dt, global::System.TimeSpan value) => dt.Add(value),
(global::System.DateTime dt, double value) => dt.AddDays(value),
(global::System.DateTime dt, double value) => dt.AddHours(value),
(global::System.DateTime dt, double value) => dt.AddMilliseconds(value),
(global::System.DateTime dt, double value) => dt.AddMinutes(value),
(global::System.DateTime dt, int months) => dt.AddMonths(months),
(global::System.DateTime dt, double value) => dt.AddSeconds(value),
(global::System.DateTime dt, long value) => dt.AddTicks(value),
(global::System.DateTime dt, int value) => dt.AddYears(value),
(global::System.DateTime dt, global::System.DateTime value) => dt.Subtract(value),
(global::System.DateTime dt, global::System.TimeSpan value) => dt.Subtract(value),
(global::System.DateTime dt, global::System.DateTime value) => dt.CompareTo(value),
(global::System.DateTime dt, global::System.DateTime value) => dt.Equals(value),
(global::System.DateTime dt, global::System.TimeSpan ts) => dt + ts,
(global::System.DateTime dt, global::System.TimeSpan ts) => dt - ts,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 - dt2,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 == dt2,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 != dt2,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 < dt2,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 <= dt2,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 > dt2,
(global::System.DateTime dt1, global::System.DateTime dt2) => dt1 >= dt2,
}.ToReadOnly();
Console.WriteLine(x);
}
} It's less bad, but still pretty bad. IT takes "only" 23 seconds for the |
I know you are investigating this prior to exploring it as a Roslyn issue, and obviously it should move to that repo if it is a Roslyn issue. Looking over it, I had some questions, which you could answer now or if you explore it as a Roslyn issue.
|
@chethusk |
is there a full repro for this? Ideally something selfcontained at a git repo, so we could pull down and trace. |
We are working on isolating it properly to make a standalone repro, but we're not quite there yet (and I'm on vacation at the moment...) |
To answer @KathleenDollard 's questions: The reaqtor/Nuqleon/Core/LINQ/Nuqleon.Linq.Expressions.Optimizers/System/Linq/Expressions/MemberTable.cs Line 23 in 4fca291
I'm not quite sure what a "LINQ" to it would mean so I'm hoping a link will suffice. I am using Visual Studio 2022 version 17.7.6, although note that we see the hang on the build agent, so we see perf problems around this code both inside VS and outside. We are currently using .NET SDK 7.0.403. We do not have a We know the .NET 5 era version of the We did not add any new analyzers. The trigger for the performance issues causing us a problem was fixing our build process so that we use the copy of
That was probably a rather inaccurate description of the change... Here's an attempt to describe it better. For ages, we had this in our pipeline: Lines 85 to 105 in 51fb8bd
Note that although this installs the 7.0 SDK, the step that runs
You can see that happening in this build: When we realised that we just needed to use the version in the SDK, we made this change: e9012e6 And that was when we started seeing these performance problems. So actually I think that first sentence is completely wrong—although we target .NET 6.0, we're using the .NET 7.0 SDK, so we went straight from version '5.1.250801' of the |
@CyrusNajmabadi but if you don't want to wait for me to come back to work on Monday, the first two steps described in #138 (comment) describe how to build a project from scratch that repros the problem. The resulting project just has a couple of files in it, and doesn't require you to download all of the Reaqtor repo. (Those steps do include a dependency on Reaqtor, but via NuGet. I have a project on my computer that does away completely with the dependency on Reaqtor, and that's what I was aiming to share next week, because I thought it would be more helpful to demonstrate the issue in a no-dependencies project.) |
i'm going on vacation for the next week. So i can only help after that. If anyone wants to just profile what's going with perfview, it would likely show where the costs are. |
Repro now available here: https://github.com/idg10/repro-slow-ide0001 |
Thanks for getting that into a self contained project. Unfortunately I'm not able to reproduce the problem. I cloned the project, ran > $d = [DateTime]::UtcNow; dotnet format; [DateTime]::UtcNow - $d;
Days : 0
Hours : 0
Minutes : 0
Seconds : 5
Milliseconds : 428
Ticks : 54286000
TotalDays : 6.28310185185185E-05
TotalHours : 0.00150794444444444
TotalMinutes : 0.0904766666666667
TotalSeconds : 5.4286
TotalMilliseconds : 5428.6 Was concerned possibly IDE001 wasn't running so I created an editorconfig file, enabled it there but got the same results. The compiler is sending ~10% of it's time in method group infererence / overload resolution. That's not too surprising given the nature of the code. To hit the particular timeout's you're referring to it would need to have a lot higher concentration. |
Hi @jaredpar Thanks for looking into this. I have discovered that since I upgraded Visual Studio 2022 to 17.8, the repro in the form I supplied to you no longer works for me either. However... The problem continues to occur on the Reaqtor solution itself. So a colleague and I looked into this a little more. He still had an older VS install than me, and was still able to repro the problem. So we then dug a bit deeper to find out what exactly was different—what DLLs were different across our two machines. In the version of the repro we were using (which is slightly different from the one I uploaded—our working one includes a BenchmarkDotNet project) we discovered one slightly weird difference:
My machine didn't have any .NET 7.0 SDKs on it this morning. (It would have done back before .NET 8.0 shipped. But I think when I updated to VS 17.8.0, the VS installer removed any .NET 7.0.x SDKs that were present.) After I reinstalled .NET SDK 7.0.203 (which is rather old, but is the version that ended up getting used on my colleague's machine) I was once again able to repro the problem. So as far as I can tell, on the latest VS 2022, then unless you've explicitly installed a .NET 7.0 SDK, the .NET 7.0 project in my repo actually ends up getting processed by a .NET 6.0 SDK, which doesn't appear to have this perf problem. I then tried removing the .NET 7.0.203 SDK, and then upgrading the repro project to use .NET 8.0. With the project on .NET 8.0, once again it takes about 40 seconds on my machine for it to process me typing Ctrl+C with the cursor on line 59. And if I delete and then paste back in line 59, it stays like this for about a half a minute: before finally resolving to this: Note that the However, running TLDR:
I've pushed an update to the repo so it's now on .NET 8.0. |
OK, I've made another change, and with this one I'm able to repro slowness easily with
I see this reported time:
To do this, I've made A couple of thing:
That last point is frustrating because I was hoping to get a more detailed report from a profiler by running the analysis in the context of the Roslyn project itself. (Since PDB files don't seem to be available for It's also a bit odd because it implies that these analyzers run slowly only when run in particular contexts. (Although I guess it could be that I've just misdiagnosed which analyzer_ is really the slow one.) |
@CyrusNajmabadi, @sharwell think analyzers / IDE will need to take a look at this. I can see the slow down with the latest repro. Mine is a bit faster: 10 seconds to ~13 but guessing that is just machine difference. Looking at a binlog from a build the compiler is executing how I would expect here. I grabbed a trace of the |
This is a known slow scenario for IDE0001. You should be able to work around the problem by adding this:
And then replacing all the uses of There is also a slight possibility things are improved by adding explicit return types to all of the lambdas. We've been working on reducing the binding work associated with fully-specified lambda signatures. You would definitely see an improvement if the collection initializer was broken up into a long series of |
Since switching to the .NET 6.0 version of
dotnet format
, ran into problems with the IDE0001 analyzer running extremely slowly (causing a build to time out) in these projects:Nuqleon.Linq.Expressions.Optimizers
Tests.Nuqleon.Linq.CompilerServices.Optimizers
We determined that IDE0001 was the one taking the time by attaching a debugger and seeing what it was doing. In files like
PureMemberCatalog.cs
it was taking half a second or more to process each line of this form:The cumulative effect of this was to cause the
dotnet format
check to take well over an hour to complete, causing it to fail due to timeout on the build server.We tried suppressing the relevant rule but this had no effect on analysis time for these projects. Perhaps IDE0001 isn't the only analyzer having a problem with this project. Or perhaps it still does the analysis and the settings would just suppress any warnings (and there are no diagnostics reported in this case).
We attempted to disable analysis completely for these projects but that also didn't help, for reasons we don't understand. (Possibly
dotnet format
just runs it regardless.)We realised that the slow performance was caused by the fact that the
PureMemberCatalog
class has a nestedSystem
class, and large numbers of lines of code like the one shown above, i.e.:We think it is something to do with the combination of:
PureMemberCatalog
) being defined in a namespace inSystem
(specificallySystem.Linq.Expressions
)System
(so that'sSystem.Linq.Expressions.PureMemberCatalog.System
)global::System...
Although it might be only that last part, because experiments to move it all out into a separate namespace made no difference. It might just be that collection initializers with very large numbers of lambdas being processed as expression trees hit this problem.
This means we really have no option but to disable this build step completely.
That's highly undesirable, so we'd like to reinstate it at some point, but this means understanding why the analyzers have such problems with this code. Ideally the analyzer performance would be fixed...but if we could change something that will work around it, that'd also be an option.
We should start by following the suggestions at https://www.meziantou.net/understanding-the-impact-of-roslyn-analyzers-on-the-build-time.htm and it is probably also worth attaching a profiler to the relevant
dotnet format
command. We can run it from the command line thus:The text was updated successfully, but these errors were encountered: