I’ve been spending a lot of time lately looking at the performance of the Visual Studio project system for C# and VB. And something that’s come up more than a few times now is consumers of the automation interfaces (a.k.a. DTE) accidentally tanking Visual Studio performance when working with project references.
To step back and review for a moment, one of the fundamental jobs the project system has is to run what are called design-time builds to determine the full contents of a project. I talked about it more here, but the short version of the story is that a design-time build is a build that the project system runs in the background that doesn’t actually call the compiler, just captures everything that makes up the build. This information is then (primarily) used to initialize the language service that provides things like Intellisense. The problem is that if your build happens to be pretty large or slow, these design-time builds can hang the Visual Studio UI with these really annoying pauses.
So, what does that have to do with references? Well, one of the uses of design-time builds is to help the project system figure out exactly what the references in a project point to. Most project references don’t specify the full path of the thing that they point at. As a result, the project system usually needs to run a design-time build so it can understand exactly where a reference is going to point once MSBuild is done with it.
Again, most of the time you never really notice this going on, it just happens quietly in the background. But there are circumstances where it can become visible, particularly when a project has unresolvable references. An unresolvable reference is, basically, a reference that the design-time build can’t find. Maybe the assembly it points at got deleted. Maybe the package it points at isn’t available on NuGet. Maybe there’s some typo in the reference itself. Whatever the reason, sometimes projects have references that cannot be resolved no matter how many design-time builds you run.
Unfortunately, this can really bite unwary Visual Studio extension developers. Because when an extension asks for the path to an unresolved reference, the project system does it’s best to try and answer that question. And how does it do it? By running a new design-time build. And if there are other unresolved references? You’ll get a design-time build for each of them. Again, if the project is small, nobody will likely notice. But if the project is large or complex? Pauses and delays.
Thankfully, there is a solution. Instead just asking for a reference’s path like this:
foreach (Reference reference in vsProject.References)
// Ack! This may cause a design-time build to happen and block the UI
// Do something....
You can do this:
foreach (Reference3 reference in vsProject.References)
// Whew. If the reference isn't resolved, we don't try to get the path
if (reference.Resolved && File.Exists(reference.Path))
// Do something....
(Note that you have to switch from using the Reference type to the Reference3 type to get to the Resolved property.)
We’ve already fixed two instances of this pattern internal to Visual Studio that were causing pauses in the UI, and we’re actively looking for others. If you’re an extension author, check your code for this pattern — it may fix pauses that your users were encountering!
(We’re also adding code to the project system to recognize situations where unresolved references are never going to resolve, and skipping the design-time builds in those cases. That should help unwary extension authors, but won’t fix cases that could possible resolve but aren’t for some reason.)
You can also follow me on Twitter here.