Skip to content
This repository was archived by the owner on Jun 30, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Boost performance by reusing MSBuildWorkspace in project metadata reader
The external process should be longer lived so that the MSBuildWorkspace.Create
call (by far the most expensive since it needs to set up a whole new MEF host
with all Roslyn services, in addition to resolving the MSBuild framework assemblies
and all SDK-style targets and assemblies too) can be reused across calls to
ReadProject (renamed to OpenProject).

By switching to the JsonRpc style, the external process can live as long as the
build (or the AppDomain?) and maintain a single instance of the MSBuildWorkspace,
which then is reused across OpenProject calls. The RPC interface exposed allows
creating new workspaces on demand (disposing the existing one automatically, so
that it can live for the entire duration of the VS AppDomain eventually.

In addition, the console app now triggers a background resolving of all the
relevant assemblies and MEF eagerly, so that by the time the actual project
reading happens, all of the Roslyn components have already warmed up.

This combination of optimizations brought down the time for reading the test
project and all its project references from 20+ seconds (with the single-use
commandline for each project) to ~5 seconds (not counting the warm-up period
of ~2 seconds).
  • Loading branch information
kzu committed Sep 23, 2018
commit 277e53816a48bf1ba6ca51228d169176fac48ad0
35 changes: 34 additions & 1 deletion src/Stunts.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,38 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stunts.Sdk", "Stunts\Stunts
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stunts.Tests", "Stunts\Stunts.Tests\Stunts.Tests.csproj", "{1320496D-6348-42B0-95F5-BA7891EF0096}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stunts.CodeAnalysis", "Stunts\Stunts.CodeAnalysis\Stunts.CodeAnalysis.csproj", "{3C2F7D4C-D10A-439C-B986-FF51664CD895}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stunts.Analyzer", "Stunts\Stunts.Analyzer\Stunts.Analyzer.csproj", "{3C2F7D4C-D10A-439C-B986-FF51664CD895}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "Samples\Sample\Sample.csproj", "{F801B6E7-70DA-46FE-937D-199BB00E7647}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F21FA4AF-78AF-49BA-8D5E-C45C95365326}"
ProjectSection(SolutionItems) = preProject
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
..\NuGet.Config = ..\NuGet.Config
EndProjectSection
EndProject
Project("{5DD5E4FA-CB73-4610-85AB-557B54E96AA9}") = "Stunts.Package", "Stunts\Stunts.Package\Stunts.Package.nuproj", "{91F9A526-E62C-491B-8774-88A61BDF1E1A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stunts.CodeFix", "Stunts\Stunts.CodeFix\Stunts.CodeFix.csproj", "{AE3450C8-CB6E-4DA9-A0F0-977F82FFE6F9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stunts.Tasks", "Stunts\Stunts.Tasks\Stunts.Tasks.csproj", "{F033B778-A0F8-43E6-85B9-E184DFFCF347}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{21EDC116-63A9-4C77-9BE0-16BACC5FA3AA}"
ProjectSection(SolutionItems) = preProject
build\CI.props = build\CI.props
build\PackageReferences.targets = build\PackageReferences.targets
build\Packaging.props = build\Packaging.props
build\Packaging.targets = build\Packaging.targets
build\Settings.props = build\Settings.props
build\Settings.targets = build\Settings.targets
build\Settings.Tests.props = build\Settings.Tests.props
build\Settings.Tests.targets = build\Settings.Tests.targets
build\Version.targets = build\Version.targets
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stunts.ProjectReader", "Stunts\Stunts.ProjectReader\Stunts.ProjectReader.csproj", "{D4BCD0FD-946F-4A43-A6B0-6FE3832C2B10}"

Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -55,6 +77,14 @@ Global
{91F9A526-E62C-491B-8774-88A61BDF1E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91F9A526-E62C-491B-8774-88A61BDF1E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91F9A526-E62C-491B-8774-88A61BDF1E1A}.Release|Any CPU.Build.0 = Release|Any CPU
{F033B778-A0F8-43E6-85B9-E184DFFCF347}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F033B778-A0F8-43E6-85B9-E184DFFCF347}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F033B778-A0F8-43E6-85B9-E184DFFCF347}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F033B778-A0F8-43E6-85B9-E184DFFCF347}.Release|Any CPU.Build.0 = Release|Any CPU
{D4BCD0FD-946F-4A43-A6B0-6FE3832C2B10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4BCD0FD-946F-4A43-A6B0-6FE3832C2B10}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4BCD0FD-946F-4A43-A6B0-6FE3832C2B10}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4BCD0FD-946F-4A43-A6B0-6FE3832C2B10}.Release|Any CPU.Build.0 = Release|Any CPU
{AE3450C8-CB6E-4DA9-A0F0-977F82FFE6F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AE3450C8-CB6E-4DA9-A0F0-977F82FFE6F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AE3450C8-CB6E-4DA9-A0F0-977F82FFE6F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -63,6 +93,9 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{21EDC116-63A9-4C77-9BE0-16BACC5FA3AA} = {F21FA4AF-78AF-49BA-8D5E-C45C95365326}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BA56F22C-8978-49E8-B6C1-5D28B14D6C1D}
EndGlobalSection
Expand Down
5 changes: 5 additions & 0 deletions src/Stunts/Stunts.ProjectReader/Empty.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>
209 changes: 75 additions & 134 deletions src/Stunts/Stunts.ProjectReader/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,169 +4,110 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.MSBuild;
using Mono.Options;
using StreamJsonRpc;

namespace Stunts
{
internal class Program
{
public static async Task<int> Main(string[] args)
{
var help = false;
var instance = default(VisualStudioInstance);
var msbuild = default(string);
var wait = false;
var project = default(string);
var properties = new Dictionary<string, string>();
var configuration = default(string);
var platform = default(string);
var output = default(string);

var options = new OptionSet
{
{ "p|project=", "MSBuild project file to read", p => project = p },
{ "i|installation:", "Visual Studio installation directory", v => instance =
MSBuildLocator.QueryVisualStudioInstances(new VisualStudioInstanceQueryOptions { DiscoveryTypes = DiscoveryType.VisualStudioSetup })
.FirstOrDefault(x => x.VisualStudioRootPath.Equals(v, StringComparison.OrdinalIgnoreCase)) ??
throw new ArgumentException($"Did not find a Visual Studio installation at {v}.") },
{ "m|msbuild:", "MSBuild root directory.", m => msbuild = m },
{ "c|configuration=", "Project configuration (i.e. Debug or Release)", c => configuration = c },
{ "t|platform=", "Project target platform (i.e. AnyCPU)", plat => platform = plat },
{ "o|output:", "Optional output file to emit. Emits to standard output if not specified", o => output = o },
{ "<>", "Additional MSBuild global properties to set", v =>
{
var values = v.Split(new[] { '=', ':' }, 2);
properties[values[0].Trim()] = values[1].Trim().Trim('\"');
}
},
new ResponseFileSource(),
{ "d|debug", "Attach a debugger to the process", d => Debugger.Launch() },
{ "w|wait", "Wait before exiting the process", w => wait = w != null },
{ "h|?|help", "Show this message and exit", h => help = true },
};

List<string> extra;
var appName = Path.GetFileName(Assembly.GetExecutingAssembly().ManifestModule.FullyQualifiedName);

try
{
extra = options.Parse(args);

if (help ||
string.IsNullOrEmpty(project) ||
(instance == null && msbuild == null))
{
var writer = help ? Console.Out : Console.Error;
private static ManualResetEventSlim exit = new ManualResetEventSlim();
private static bool ready;

// show some app description message
writer.WriteLine($"Usage: {appName} [OPTIONS]+");
writer.WriteLine();
public static int Main(string[] args)
{
if (args.Any(s => s.Equals("-d")) || args.Any(s => s.Equals("/d")))
Debugger.Launch();

writer.WriteLine("Options:");
options.WriteOptionDescriptions(writer);
if (args.Length != 0 && Directory.Exists(args[0]))
// If we receive a path as a first argument, we use that for MSBuild
MSBuildLocator.RegisterMSBuildPath(args[0]);
else
// Otherwise, just use the current directory.
MSBuildLocator.RegisterMSBuildPath(Directory.GetCurrentDirectory());

return -1;
}
var rpc = new JsonRpc(Console.OpenStandardOutput(), Console.OpenStandardInput(), new Program());
rpc.StartListening();

// TODO: Mac support
if (instance != null)
MSBuildLocator.RegisterInstance(instance);
else if (msbuild != null)
MSBuildLocator.RegisterMSBuildPath(msbuild);
// Force resolving right-away.
Task.Run(Init);

var metadata = await ReadProject(project, properties, output != null);
if (string.IsNullOrEmpty(output))
metadata.Save(Console.Out);
else
metadata.Save(output);
exit.Wait();

if (wait)
Console.ReadLine();
return 0;
}

return 0;
}
catch (OptionException e)
{
Console.Error.WriteLine($"{appName}: {e.Message}");
Console.Error.WriteLine($"Try '{appName} -?' for more information.");
private static async Task Init()
{
var workspace = MSBuildWorkspace.Create();
await workspace.OpenProjectAsync(
Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().ManifestModule.FullyQualifiedName), "Empty.csproj"));
ready = true;
}

if (wait)
Console.ReadLine();
private MSBuildWorkspace workspace;

return -1;
}
catch (Exception e)
{
#if DEBUG
if (!Debugger.IsAttached)
Debugger.Launch();
#endif
public void Debug() => Debugger.Launch();

// TODO: should we render something different in this case? (non-options exception?)
Console.Error.WriteLine($"{appName}: {e.Message}");
public void Exit() => exit.Set();

if (wait)
Console.ReadLine();
public bool Ping() => ready;

return -1;
}
public void CreateWorkspace(Dictionary<string, string> properties)
{
workspace?.Dispose();
workspace = MSBuildWorkspace.Create(properties);
}

/// <summary>
/// Read the given project file with the specified global
/// properties.
/// </summary>
private static async Task<XElement> ReadProject(string projectFile, Dictionary<string, string> properties, bool consoleProgress)
public void CloseWorkspace() => workspace?.Dispose();

public async Task<object> OpenProject(string projectFile)
{
var workspace = MSBuildWorkspace.Create(properties);
var project = await workspace.OpenProjectAsync(projectFile, consoleProgress ? new ConsoleProgressReporter() : null);
var references = project.MetadataReferences.OfType<PortableExecutableReference>().ToList();
if (workspace == null)
CreateWorkspace(new Dictionary<string, string>());

return new XElement("Project",
new XAttribute("Id", project.Id.Id),
new XAttribute("Name", project.Name),
new XAttribute("AssemblyName", project.AssemblyName),
// We may support other languages than C# in our visitors down the road.
new XAttribute("Language", project.Language),
new XAttribute("FilePath", project.FilePath),
new XAttribute("OutputFilePath", project.OutputFilePath),
new XElement("CompilationOptions",
new XAttribute("OutputKind", project.CompilationOptions.OutputKind.ToString()),
new XAttribute("Platform", project.CompilationOptions.Platform.ToString())),
new XElement("ProjectReferences", project.ProjectReferences
.Where(x => workspace.CurrentSolution.Projects.Any(p => p.Id == x.ProjectId))
.Select(x => new XElement("ProjectReference",
new XAttribute("FilePath", workspace.CurrentSolution.Projects.First(p => p.Id == x.ProjectId).FilePath)))),
new XElement("MetadataReferences", references.Select(x =>
new XElement("MetadataReference", new XAttribute("FilePath", x.FilePath)))),
new XElement("Documents", project.Documents.Select(x =>
new XElement("Document",
new XAttribute("FilePath", x.FilePath),
new XAttribute("Folders", string.Join(Path.DirectorySeparatorChar.ToString(), x.Folders))))),
new XElement("AdditionalDocuments", project.AdditionalDocuments.Select(x =>
new XElement("Document",
new XAttribute("FilePath", x.FilePath),
new XAttribute("Folders", string.Join(Path.DirectorySeparatorChar.ToString(), x.Folders)))))
);
}
var project = workspace.CurrentSolution.Projects.FirstOrDefault(p => p.FilePath == projectFile) ??
await workspace.OpenProjectAsync(projectFile);
var references = project.MetadataReferences.OfType<PortableExecutableReference>().ToList();

private class ConsoleProgressReporter : IProgress<ProjectLoadProgress>
{
public void Report(ProjectLoadProgress loadProgress)
return new
{
var projectDisplay = Path.GetFileName(loadProgress.FilePath);
if (loadProgress.TargetFramework != null)
project.Id.Id,
project.Name,
project.AssemblyName,
project.Language,
project.FilePath,
project.OutputFilePath,
CompilationOptions = new
{
projectDisplay += $" ({loadProgress.TargetFramework})";
}

Console.WriteLine($"{loadProgress.Operation,-15} {loadProgress.ElapsedTime,-15:m\\:ss\\.fffffff} {projectDisplay}");
}
project.CompilationOptions.OutputKind,
project.CompilationOptions.Platform
},
ProjectReferences = project.ProjectReferences
.Where(x => workspace.CurrentSolution.ProjectIds.Contains(x.ProjectId))
.Select(x => workspace.CurrentSolution.Projects.First(p => p.Id == x.ProjectId).FilePath)
.ToArray(),
MetadataReferences = references.Select(x => x.FilePath).ToArray(),
Documents = project.Documents
.Select(x => new
{
x.FilePath,
x.Folders
})
.ToArray(),
AdditionalDocuments = project.AdditionalDocuments
.Select(x => new
{
x.FilePath,
x.Folders
})
.ToArray()
};
}
}
}
6 changes: 5 additions & 1 deletion src/Stunts/Stunts.ProjectReader/Stunts.ProjectReader.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="$(RoslynVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Features" Version="$(RoslynVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="$(RoslynVersion)" />
<PackageReference Include="Mono.Options" Version="5.3.0.1" />
<PackageReference Include="StreamJsonRpc" Version="1.3.23" />
</ItemGroup>

<ItemGroup>
<None Include="Empty.csproj" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<Target Name="_RemoveLocalizedResources" AfterTargets="ResolvePackageAssets">
Expand Down
Loading