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

[WIP] Improve solution load performance #2909

Closed
wants to merge 3 commits into from

Conversation

saul
Copy link
Contributor

@saul saul commented Apr 24, 2017

Fixes #2793, fixes #2916, fixes #1944.

This is a very WIP pull request that improves solution load performance by moving ComputeSourcesAndFlags from being a UI thread operation to a background thread operation.

The design consists of a new ProjectFlagsService which processes a queue of projects that are dirty and need their sources/flags recalculating. It takes an exclusive lock on VS's design-time build and then runs an asynchronous build of the project (stopping just before invoking the compiler).

This PR also contains changes to ProjectsSitesAndFiles options caching. These changes mean that the Dense solution loads in 20 seconds instead of hanging VS forever. The ProjectsSitesAndFiles changes brought the load time down to 2:50mins, and the off-loading of ComputeSourcesAndFlags brings it down further to 20 seconds.

Remaining work

  • Investigate whether Roslyn workspace setup can now be made synchronous
  • Revert changes conflicting with use graph of option objects - fix 2793 #3002
  • Use MEF for ProjectFlagsService
  • Prioritise the currently viewed project when the active document is changed
  • Be resilient to Path.GetFullPath failing in reference node properties
  • Fix sources and flags results not being passed down to the background compiler

@saul
Copy link
Contributor Author

saul commented Apr 24, 2017

I could do with a hand with the following issues:

  • I can't get the ProjectFlagsService registered with MEF. When I do GetService on the site service provider in the ProjectSystem, it cannot find the service.
  • When InvalidateConfiguration is called on the background request when the sources and flags have been calculated by MSBuild, it doesn't seem to update IntelliSense (still squiggles everywhere)

@@ -208,7 +209,7 @@ type
CodeSense = true,
DefaultToNonHotURLs = true,
EnableCommenting = true,
CodeSenseDelay = 100,
CodeSenseDelay = 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Need to actually investigate if it's made a difference...

@forki
Copy link
Contributor

forki commented Apr 24, 2017

is there already noticable difference?

@saul
Copy link
Contributor Author

saul commented Apr 24, 2017

@forki with regards to what?

open Microsoft.Build.Execution

(*
[<Export(typeof<ProjectFlagsService>)>]
Copy link
Contributor

@majocha majocha Apr 24, 2017

Choose a reason for hiding this comment

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

Just [<Export>] would do?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Under the hood it still just uses ExportAttribute though - so I doubt that would help. Thanks for the input though :)

type internal ProjectFlagsService
[<ImportingConstructor>]
(
[<Import(typeof<SVsBuildManagerAccessor>)>] accessor : IVsBuildManagerAccessor
Copy link
Contributor

@majocha majocha Apr 24, 2017

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@saul saul Apr 24, 2017

Choose a reason for hiding this comment

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

Thanks, I'll give that a shot tomorrow.

Also in project.fs it looks like I need to call GetService on IComponentModel instead of on this.Site to actually get a reference to ProjectFlagsService 👍

See https://github.com/Microsoft/visualfsharp/blob/9a6ac3da4249ae5c397d331c99c23eea8fb37bdc/vsintegration/src/FSharp.Editor/Options/UIHelpers.fs#L24

Copy link
Contributor

Choose a reason for hiding this comment

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

I experimented a bit and it turns out exports from other assemblies like FSharp.Editor work fine and are resolved with IComponentModel. Anything declared here fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's bizarre, thanks for helping me with this. @cartermp do you know anyone who could weigh in/explain why our project system assembly doesn't seem to be linked up to MEF?

Copy link
Contributor

Choose a reason for hiding this comment

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

I have no idea. @dsyme would you know, or know who might know?

Copy link
Contributor

Choose a reason for hiding this comment

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

@saul @cartermp I'd ask @OmarTawfik, @brettfo, @KevinRansom . It may be just historical. Note I know almost nothing about MEF

Copy link
Contributor

Choose a reason for hiding this comment

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

@@ -1558,7 +1608,7 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem
Debug.Assert(System.Threading.Thread.CurrentThread.ManagedThreadId = uiThreadId, "called GetProjectSite() while still in Opening state from non-UI thread")
#endif
x.ComputeSourcesAndFlags()
if not(projectSite.State = ProjectSiteOptionLifetimeState.Opened) then
if projectSite.State <> ProjectSiteOptionLifetimeState.Opened then
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 :)

|> Seq.map(fun ref -> (getProject ref.ProjectId).FilePath)
|> hashSetIgnoreCase

for referencedSite in do
Copy link
Contributor

Choose a reason for hiding this comment

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

updatedProjects.Except(workspaceProjects) ?

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 I was wondering that... will change them to use that tomorrow 👍

@forki
Copy link
Contributor

forki commented Apr 24, 2017

@saul I guess my question is if this work already shows performance benefits for your projects.

@saul
Copy link
Contributor Author

saul commented Apr 24, 2017

@forki as in the PR description:

These changes mean that the Dense solution loads in 20 seconds instead of hanging VS forever. The ProjectsSitesAndFiles changes brought the load time down to 2:50mins, and the off-loading of ComputeSourcesAndFlags brings it down further to 20 seconds.

So yes, in large solutions with dense dependency graphs, it's essentially gone from impossible to load to 20 seconds 😃

let solution = workspace.CurrentSolution
let documentIds = solution.GetDocumentIdsWithFilePath(fileName)
if not documentIds.IsEmpty then
analyzerService.Reanalyze(workspace, documentIds=documentIds, highPriority=true)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also wondering if this is going to get us faster squiggles in the editor. Will probably move this change out of this PR though.

@@ -316,29 +328,50 @@ and
projectContext.RemoveSourceFile(file)
updated <- true

let updatedRefs = site.AssemblyReferences() |> hashSetIgnoreCase
(*
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will be put back - it's commented out as it's causing issues at the moment.

Copy link
Contributor

Choose a reason for hiding this comment

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

I may be wrong but IIRC project updates must run on UI thread, contrary to what some comments in Roslyn say.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's fine with this PR - the sources/flags notifier (which eventually calls through to this) is running on the UI thread.

@@ -697,7 +697,7 @@ public override string Url

public void SetDebugLogger(Microsoft.Build.Framework.ILogger debugLogger)
{
myDebugLogger = debugLogger;
//myDebugLogger = debugLogger;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Temporary change, should not have committed

@forki
Copy link
Contributor

forki commented Apr 24, 2017

How could I miss that. Probably just read the email.
Anyways: that's awesome! 💋

@saul
Copy link
Contributor Author

saul commented Apr 24, 2017

Yeah I added the perf stats in after I posted. I assume you've got the repo setup to email you immediately whenever there's action 😆

@saul
Copy link
Contributor Author

saul commented Apr 25, 2017

With regards to being able to export/get the ProjectFlagsService - any more ideas on this? Even if I use just [<Export>] and try to get the service via IComponentModel, it can never find it. I've no idea what I'm doing wrong - it seems to be exactly how other projects like PTVS and the GitHub VS extension use MEF.

cc @majocha

@majocha
Copy link
Contributor

majocha commented Apr 25, 2017

@saul from what I gather ProjectPackage, where you want to import it, is not MEF composed itself. To get the ProjectFlagsService there you would need to go through SComponentModel. I tried to get SComponentModel service and then import a small dummy part. It fails, so it's not just ProjectFlagsService.
Is it worth the trouble here? Do you plan on reusing ProjectFlagsService somewhere else?

@saul
Copy link
Contributor Author

saul commented Apr 25, 2017

@majocha yeah I was hoping to use it in FSharp.Editor. I want to call PrioritiseProject when a document is opened.

…ion (add support changing project references)

Fixes dotnet#2916, fixes dotnet#1944
@saul
Copy link
Contributor Author

saul commented Apr 25, 2017

In the latest commit I've fixed #1944 and #2916 as I moved some LanguageService code over to use IVsRunningDocumentTable instead of hooking into SetupNewTextView (that's been removed entirely). I've also re-enabled the Roslyn workspace syncronisation that I disabled in the previous commit.

Also some more stats - the VisualFSharp solution now loads in 10 seconds (down from 20).

@forki
Copy link
Contributor

forki commented Apr 25, 2017 via email

@vasily-kirichenko
Copy link
Contributor

Awesome.

@vasily-kirichenko
Copy link
Contributor

FSharp.Compiler project cannot load with this PR:

image

@saul
Copy link
Contributor Author

saul commented Apr 25, 2017

@vasily-kirichenko I believe that's because some of the references aren't of a legal form. I'm now calling Path.GetFullPath to normalise the paths (some were like C:\Foo\Bar....\Something... and so were failing the equality check)

@vasily-kirichenko
Copy link
Contributor

@saul I tried to load FSharp.Configuration - it loaded insanely fast, but nothing works (coloring, etc.), no errors anywhere.

@saul
Copy link
Contributor Author

saul commented Apr 25, 2017

@vasily-kirichenko @dsyme I think I've found the cause of why you aren't getting semantic colourisation. (I do get block structure and syntactic). See the reactor log https://gist.github.com/saul/bc9d2ef16d8d9b43e4c44f2d2c211b9b

Search that log for long running tasks. Especially noteworthy:

  • Reactor: <-- background step, remaining 41, took 324170.6876ms
  • Reactor: <-- ParseFileInProject C:\Code\visualfsharp\vsintegration\src\FSharp.Editor\LanguageService\LanguageService.fs, remaining 35, took 85086.3554ms
  • Reactor: <-- ParseAndCheckFileInProject C:\Code\visualfsharp\vsintegration\src\FSharp.Editor\LanguageService\LanguageService.fs, remaining 12, took 61422.0086ms
  • Reactor: <-- ParseAndCheckFileInProject C:\Code\visualfsharp\vsintegration\src\FSharp.Editor\LanguageService\LanguageService.fs, remaining 3, took 16677.1243ms

I've no idea why these are taking so long... time to break out the profiler? 😦

}

async {
while true do
Copy link
Contributor

Choose a reason for hiding this comment

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

This basically looks very clear. But I'd somehow like to see some safeguards where we try/catch any exceptions in chunks of this code. Infinite loops starting mailbox processors make me nervous if exceptions might happen.

And are we sure the solution events get raised reliably?

Basically I'm wondering "what can go catastrophically wrong that can cause this service to malfunction in really bad ways"

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'm confident that the solution events get raised reliably, yes.

With regards to try/catches, where abouts would you like those? It's worth noting that the project system DoMSBuildSubmission is already very resilient to exceptions and will always call our callback on the UI thread.


// Keep trying to grab the exclusive lock for a design time build
while accessor.BeginDesignTimeBuild() |> ErrorHandler.Failed do
do! Async.Sleep 2000
Copy link
Contributor

Choose a reason for hiding this comment

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

2sec is a long pause - does it need to be so long?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No I'll reduce this to less than a second.

// Having an exclusive lock on design-time builds means the user
// can't initiate any builds themselves. Wait a few seconds before
// trying to grab one again.
do! Async.Sleep 2000
Copy link
Contributor

Choose a reason for hiding this comment

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

Likewise - why such a comparatively long pause?

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 leaves some time so that the user can initiate builds/move files around in the Solution Explorer. When a design-time build is running, quite a few parts of the UI are locked out.

@@ -121,26 +127,38 @@ type internal ProjectSitesAndFiles() =
| _ -> None)
| None -> Seq.empty

static let projectCache = ConcurrentDictionary<string,FSharpProjectOptions>()
Copy link
Contributor

Choose a reason for hiding this comment

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

What removes entries from this cache?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nothing yet - good catch

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@dsyme
Copy link
Contributor

dsyme commented May 8, 2017

@saul Please take a look at #3002 for a different fix.

If we went with that fix would you want to keep this? It could still be valuable since you mention other bug fixes?

UnresolvedReferences = None
OriginalLoadReferences = []
ExtraProjectInfo=extraProjectInfo }
and getProjectOptionsForProjectSite(projectSite:IProjectSite, fileName:string, extraProjectInfo, serviceProvider, cacheUsage:CacheUsage) =
Copy link
Contributor

Choose a reason for hiding this comment

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

This will clash with my other proposed fix. The project options are already stored in a table in LanguageService.fs, it seems to me we should just callback to there to look up the table.

@@ -76,7 +76,7 @@ public override string Url
{
get
{
return this.myAssemblyPath;
return Path.GetFullPath(this.myAssemblyPath);
Copy link
Contributor

Choose a reason for hiding this comment

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

This fix still looks useful?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was used by the caching code to determine if two references were the same. this.myAssemblyPath is not absolute and so it made comparing them difficult when the rooted-ness was inconsistent within a solution. It's not a bug fix per se.


string targetFrameworkMoniker = (string)otherTargetFrameworkMonikerObj;
object otargetFrameworkMoniker = null;
int hr = hierarchy.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID4.VSHPROPID_TargetFrameworkMoniker, out otargetFrameworkMoniker);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the correctness fix?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No this stops an occasional assertion on startup

Copy link
Contributor

Choose a reason for hiding this comment

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

ok, thanks

| _ -> ()

member this.TrackDocument(fileName) =
match VsRunningDocumentTable.FindDocumentWithoutLocking(package.RunningDocumentTable, fileName) with
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dsyme this file essentially encapsulates the correctness fixes. However with 15.3 now in preview we can move over to the Miscellaneous Files workspace. Read the comments on dotnet/roslyn#17263 and my comment about Miscellaneous Files in this file.

@saul
Copy link
Contributor Author

saul commented May 18, 2017

@dsyme I'll port over the RunningDocumentTable changes that fix #2916 and #1944 to a smaller PR in due course 😄

@dsyme
Copy link
Contributor

dsyme commented May 23, 2017

@dsyme I'll port over the RunningDocumentTable changes that fix #2916 and #1944 to a smaller PR in due course 😄

That would be fantastic, thanks!

@dsyme
Copy link
Contributor

dsyme commented May 30, 2018

@saul This PR is now really old, and I will close it. Can you reopen or resubmit if you want to resurect it? thanks

@dsyme dsyme closed this May 30, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
8 participants