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

run analyzers on multiple threads if allowed to #2285

Merged
merged 23 commits into from
Jan 20, 2022

Conversation

TomasEkeli
Copy link
Contributor

@TomasEkeli TomasEkeli commented Dec 2, 2021

fixes #2241

background

running analyzers is is currently sequential and can take a long time on big projects. this change can help by running
in parallel, particularly if the machine has many cores.

configuration

introduces a new option the user can set in omnisharp.json: RoslynExtensionsOptions.DiagnosticWorkersThreadCount.

  • controls how many threads the diagnostic workers will use for analysis. this allows users to control how much of their available compute they want to give the analysers access to.
  • any value up (exclusive) the number of cores on their computer would be good.
  • setting this to 1 will make the analysis run in one worker (i.e. sequentially, like before this patch)
  • not setting this value will default to using 75% of the available cores, or 1 if that is all that is available

observability

the analysers report their status as a percentage done of the total number of documents to analyse. this is done instead of listing the currently analysed project, and gives the user a better experience. thanks to @DaRosenberg for this, and help throughout!

in my experience this decreases the wall-time of analysers by a good amount at the expense of running the users' computer hotter. exposing it as a configuration allows the users to make their own trade-off.

documentation

the documentation for omnisharp.json in the wiki should be updated once this is merged.

there is now a new option in the roslyn-extensions-option:
ThreadsToUseForAnalyzers - which defaults to half the number
of processors on the machine.

there are two workers started, one for background and one for
foreground. so this *might* actually use all the cores? in my
usage it seems not to.
@dnfadmin
Copy link

dnfadmin commented Dec 2, 2021

CLA assistant check
All CLA requirements met.

Copy link
Contributor

@DavidZidar DavidZidar left a comment

Choose a reason for hiding this comment

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

Seems like a simple solution, I think it might work. When I did my quick hack however I did get some timeout errors, have you noticed anything like that? I probably got timeouts because I was going full blast with no "throttler" though.

Also, there's a similar foreach loop here, do you think it might need the same treatment?

foreach (var document in documents)
{
if (document?.Project?.Name == null)
continue;
var projectName = document.Project.Name;
var diagnostics = await GetDiagnosticsForDocument(document, projectName);
results.Add(new DocumentDiagnostics(document.Id, document.FilePath, document.Project.Id, document.Project.Name, diagnostics));
}

src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs Outdated Show resolved Hide resolved
Co-authored-by: David Zidar <381720+DavidZidar@users.noreply.github.com>
@TomasEkeli
Copy link
Contributor Author

Seems like a simple solution, I think it might work. When I did my quick hack however I did get some timeout errors, have you noticed anything like that? I probably got timeouts because I was going full blast with no "throttler" though.

Also, there's a similar foreach loop here, do you think it might need the same treatment?

foreach (var document in documents)
{
if (document?.Project?.Name == null)
continue;
var projectName = document.Project.Name;
var diagnostics = await GetDiagnosticsForDocument(document, projectName);
results.Add(new DocumentDiagnostics(document.Id, document.FilePath, document.Project.Id, document.Project.Name, diagnostics));
}

nice catch, I'll look into it

same logic as for the diagnostic worker with analyzers.
since it's used for diagnostic with and without analyzers the name
should reflect this
@DavidZidar
Copy link
Contributor

I just pulled your branch and tried it out and it is indeed much faster, but I'm still getting the timeout errors I mentioned and tested this on a small/medium sized project on a 16 core Ryzen 5950X.

They look like this in the OmniSharp log (I truncated the list of analyzers because it is very long):

[fail]: OmniSharp.Roslyn.CSharp.Services.Diagnostics.CSharpDiagnosticWorkerWithAnalyzers
        Analysis of document SomeController.cs failed or cancelled by timeout: The operation was canceled., analysers: Microsoft.CodeAnalysis.UseSystemHashCode.UseSystemHashCodeDiagnosticAnalyzer, Microsoft.CodeAnalysis.UseExplicitTupleName.UseExplicitTupleNameDiagnosticAnalyzer, Microsoft.CodeAnalysis.MakeFieldReadonly.MakeFieldReadonlyDiagnosticAnalyzer, Microsoft.CodeAnalysis.Formatting.FormattingDiagnosticAnalyzer, ...

The timeout seems to be this one which is only 10 seconds, I'm thinking the default value would have to be increased when running things in parallel. Maybe 30 seconds?

public int DocumentAnalysisTimeoutMs { get; set; } = 10 * 1000;

@TomasEkeli
Copy link
Contributor Author

ah, I've had that at 30 seconds for a long time, that's probably why I wasn't seeing the timeouts.

is there a problem if we just increase the default to that?

@TomasEkeli
Copy link
Contributor Author

i've set it to 30s default.

TomasEkeli and others added 7 commits December 4, 2021 17:20
i should never try to do this stuff from github
- Parallelize at the document level instead of only at the project level (so user benefits also with few projects but many files)
- Since currently analyzing project no longer has any real meaning, add new status events to convey "analyzing n remaining files", updates every 50 documents
- Retain old status events for backward compat with clients
- Use 75% of available cores instead of half
@DaRosenberg
Copy link
Contributor

I opened an alternative PR #2312 with some further improvements upon this work.

DaRosenberg and others added 2 commits December 19, 2021 14:19
hard-coding to a number of documents (e.g. 50 or 24) will either send
updates very rarely giving large jumps, or very frequently (i.e. many
times within one percentage-point).

this change calculates how many documents correspond to 1%, and
send an event every time that has been reached. if that is less than
every 10 documents, it events on every tenth document.

this avoids unnecessary updates and updates as often as relevant.
@TomasEkeli
Copy link
Contributor Author

can we get a review on this @filipw or someone with write access? just picked filip to mention as he's the top contributor and probably know who would be right.

@JoeRobich
Copy link
Member

@TomasEkeli Can you update the PR description to describe the changes the code is currently making. I didn't see a new RoslynOption for number of analyzer threads for instance.

There are certainly multiple places we could add parallelism to the Analyzer runner. In AnalyzeDocument() we could set concurrentAnalysis to true when getting the CompilationWithAnalyzers. However, with that approach, we don't have any knobs to control how many resources it will consume. I think the approach in this PR is OK, since it gives O# the ability to decide how to use resources.

@TomasEkeli
Copy link
Contributor Author

TomasEkeli commented Jan 12, 2022

@TomasEkeli Can you update the PR description to describe the changes the code is currently making. I didn't see a new RoslynOption for number of analyzer threads for instance.

There are certainly multiple places we could add parallelism to the Analyzer runner. In AnalyzeDocument() we could set concurrentAnalysis to true when getting the CompilationWithAnalyzers. However, with that approach, we don't have any knobs to control how many resources it will consume. I think the approach in this PR is OK, since it gives O# the ability to decide how to use resources.

i've updated the pull-request description, @JoeRobich - it was a little outdated. sorry about that, and i hope it's clearer now.

@DaRosenberg
Copy link
Contributor

@JoeRobich We did consider setting concurrentAnalysis to true as well, but decided not to because we found this mention:

https://www.csharpcodi.com/csharp-examples/Microsoft.CodeAnalysis.Diagnostics.EngineV2.DiagnosticIncrementalAnalyzer.CompilationManager.GetAnalyzerExceptionFilter(Project)/

// in IDE, we always set concurrentAnalysis == false otherwise, we can get into thread starvation due to
// async being used with syncronous blocking concurrency.

Not sure if that's applicable at all in the OmniSharp context given that it's in a separate process from the IDE, but decided not the risk it nonetheless.

Copy link
Member

@JoeRobich JoeRobich left a comment

Choose a reason for hiding this comment

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

Thanks!

@JoeRobich
Copy link
Member

CC: @filipw, in case you have any concerns

@filipw
Copy link
Member

filipw commented Jan 20, 2022

I think this should be OK because it is gated. Also, normally we hide such features behind feature flags, but since the entire analysis is behind a feature flag already, it is fine. I am a bit worried that it can lead to state corruption on the client side but we can give it a try.

Thanks a lot for your work.

@JoeRobich JoeRobich merged commit b4042a7 into OmniSharp:master Jan 20, 2022
@TomasEkeli
Copy link
Contributor Author

i'v updated the wiki

@filipw
Copy link
Member

filipw commented Jan 24, 2022

I am a bit worried that it can lead to state corruption on the client side but we can give it a try.

Unfortunately, looking at the feedback in dotnet/vscode-csharp#5017 from users using the latest build from master, it seems like there is indeed a regression where diagnostics retrieval crashes with a null reference.

@DaRosenberg
Copy link
Contributor

@filipw I can take a look later tonight. What's the correct process here, open another PR against master with the fix?

@filipw
Copy link
Member

filipw commented Jan 24, 2022

thanks, yes that is correct. Every merge to master is published as beta prerelease and users who have "omnisharp.path": "latest" set in their VS Code receive that build then on next VS Code restart. This usually allows catching regressions early 🙂

@DavidZidar
Copy link
Contributor

@filipw I'm fairly sure I found the issue and opened a PR with a fix #2333

@DaRosenberg
Copy link
Contributor

DaRosenberg commented Jan 24, 2022

@DavidZidar Yep, that's the same improvement I had previously made in the CSharpDiagnosticWorkerWithAnalyzers when I started helping out on this PR. I wasn't aware that the CSharpDiagnosticWorker had been changed also, otherwise I would have done the same there..

Anyway, kudos for tracking it down and fixing! 👍🏻

@DavidZidar
Copy link
Contributor

@DaRosenberg Ah, and a good improvement it was. :) I reviewed the code at the time but failed to consider concurrency. And thanks for the initial investigation on how to reproduce the issue, it really helped!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Analyzing projects is extremely slow compared to MSBuild or Visual Studio
6 participants