Sebastian recently reported a scenario where an operation in Visual Studio took an unreasonable amount of time (15minutes) on a large Unity project (477 individual csproj in the solution).
Sebastian was kind enough to reach out and share a way to reproduce the issue.
When all the projects of a Visual Studio solution are in an “unloaded” state.
You can load all the projects at once by right clicking on the solution and invoking the command Load all projects
.
You can get in a state where all projects are considered unloaded if they are of project types that require a Visual Studio extension that is not currently installed. In Sebastian's case, he opened a Unity project without having the Visual Studio Tools for Unity installed.
The Tools for Unity offer a “Unity Project Explorer” (affectionately called UPE) Tool Window, that displays a view of the Unity project in a similar way Unity does, folder based instead of a solution/project based view like the “Solution Explorer” does.
To react to changes to the solution and adjust the content of the UPE, we were using EnvDTE.SolutionEvents to listen to differente events, including ProjectAdded
.
The intent being, if a csproj is added to the solution, we rebuild the treeview of the UPE. In a day to day workflow, this is never done repeatedly, so this never came onto our radar.
When you click Load all projects
however, the event ProjectAdded
is going to be called for each project in the solution. In Sebastian's case, we were rebuilding the treeview of the UPE 477 times.
When we first wrote the UPE, there wasn't a good way to be notified by Visual Studio that multiple projects were going to be loaded or unloaded.
Now we can use the interface IVsSolutionBatchProjectActionEvents.
Make sure to use IVsSolutionBatchProjectActionEvents
and not the Batch
methods of IVsSolutionLoadEvents because those have been marked as Obsolete
and are not called.
The solution was as simple as replacing our usage of the EnvDTE.SolutionEvents
by a solution listener implementing the family of IVsSolutionEvents
interface and IVsSolutionBatchProjectActionEvents
.
We're now able to distinguish between an individual project load, and a batched project load. So when you do Load all projects
, we're going to be rebuilding the UPE only once.
So for Sebastian's solution, something that went from 15min for 477 projects should now take 15x60/477 => 1.88s (probably slightly more, I'm assuming rebuilding the treeview for the last project is going to take longer than for the first project simply because there will be more items). That's still too long for what it is, but it's now back in the pretty reasonable realm for as big of a project.
So we basically went from something that did this (at least in idea as we were not using exactly this interface):
public int OnAfterOpenProject(IVsHierarchy hierarchy, int added)
{
_explorer.ReloadUnityTree();
return HResult.S_OK;
}
To something that did this (code is not complete and just for illustration of the interface used):
public int OnBeforeBatchProjectLoad(IVsBatchProjectActionContext pContext)
{
_batching = true;
return HResult.S_OK;
}
public int OnAfterOpenProject(IVsHierarchy hierarchy, int added)
{
if (!_batching)
{
_explorer.ReloadUnityTree();
}
return HResult.S_OK;
}
public int OnEndBatchProjectLoad(IVsBatchProjectActionContext pContext)
{
_batching = false;
_explorer.ReloadUnityTree();
return HResult.S_OK;
}
In the future, we could definitely be smarter and not rebuild the entire UPE but compute the difference between the two states.