Skip to content

Commit aed3a7f

Browse files
committed
25x nonparallelized speedup by avoiding MSBuildWorkspace except for multi-project examples
1 parent 1731e15 commit aed3a7f

File tree

4 files changed

+175
-10
lines changed

4 files changed

+175
-10
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
5+
namespace ExampleTester;
6+
7+
internal sealed record CsprojParseResult(
8+
string? AssemblyName,
9+
string TargetFramework,
10+
CSharpParseOptions ParseOptions,
11+
CSharpCompilationOptions CompilationOptions,
12+
ImmutableArray<SyntaxTree> GeneratedSources);

tools/ExampleTester/ExampleTester.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11+
<PackageReference Include="Basic.Reference.Assemblies.Net60" Version="1.8.2" />
1112
<PackageReference Include="Microsoft.Build.Locator" Version="1.9.1" />
1213
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
1314
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" />
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using System.Collections.Frozen;
2+
using System.Collections.Immutable;
3+
using System.Globalization;
4+
using System.Xml.Linq;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.Text;
8+
9+
namespace ExampleTester;
10+
11+
internal static class FastCsprojCompilationParser
12+
{
13+
public static CSharpCompilation CreateCompilation(string csprojPath)
14+
{
15+
var csproj = ParseCsproj(csprojPath);
16+
17+
var syntaxTrees = Directory.GetFiles(Path.GetDirectoryName(csprojPath)!, "*.cs").AsParallel().Select(path =>
18+
{
19+
using var stream = File.OpenRead(path);
20+
return CSharpSyntaxTree.ParseText(SourceText.From(stream), csproj.ParseOptions, path);
21+
});
22+
23+
return CSharpCompilation.Create(
24+
csproj.AssemblyName,
25+
csproj.GeneratedSources.AddRange(syntaxTrees),
26+
SupportedTargetFrameworks[csproj.TargetFramework].References,
27+
csproj.CompilationOptions);
28+
}
29+
30+
private static readonly FrozenDictionary<string, OutputKind> SupportedOutputKinds = new KeyValuePair<string, OutputKind>[]
31+
{
32+
new("Exe", OutputKind.ConsoleApplication),
33+
new("Library", OutputKind.DynamicallyLinkedLibrary),
34+
new("WinExe", OutputKind.WindowsApplication),
35+
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
36+
37+
private sealed record TargetFrameworkInfo(
38+
ImmutableArray<PortableExecutableReference> References,
39+
LanguageVersion DefaultLanguageVersion,
40+
int DefaultWarningLevel);
41+
42+
private static readonly FrozenDictionary<string, TargetFrameworkInfo> SupportedTargetFrameworks = new KeyValuePair<string, TargetFrameworkInfo>[]
43+
{
44+
new("net6.0", new(Basic.Reference.Assemblies.Net60.References.All, LanguageVersion.CSharp10, DefaultWarningLevel: 6)),
45+
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
46+
47+
private static CsprojParseResult ParseCsproj(string filePath)
48+
{
49+
var csprojDocument = XDocument.Load(filePath);
50+
51+
string? assemblyName = null;
52+
string? targetFramework = null;
53+
var parseOptions = CSharpParseOptions.Default;
54+
var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, deterministic: true);
55+
var implicitUsings = false;
56+
57+
foreach (var element in csprojDocument.Root!.Elements())
58+
{
59+
switch (element.Name.LocalName)
60+
{
61+
case "PropertyGroup":
62+
foreach (var property in element.Elements())
63+
{
64+
switch (property.Name.LocalName)
65+
{
66+
case "OutputType":
67+
compilationOptions = compilationOptions.WithOutputKind(SupportedOutputKinds[property.Value]);
68+
break;
69+
case "TargetFramework":
70+
targetFramework = property.Value;
71+
break;
72+
case "Nullable":
73+
compilationOptions = compilationOptions.WithNullableContextOptions(
74+
Enum.Parse<NullableContextOptions>(property.Value, ignoreCase: true));
75+
break;
76+
case "AssemblyName":
77+
assemblyName = property.Value;
78+
break;
79+
case "AllowUnsafeBlocks":
80+
compilationOptions = compilationOptions.WithAllowUnsafe(bool.Parse(property.Value));
81+
break;
82+
case "LangVersion":
83+
var enumMemberName = property.Value;
84+
85+
if (decimal.TryParse(
86+
property.Value,
87+
NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite | NumberStyles.AllowDecimalPoint,
88+
CultureInfo.InvariantCulture,
89+
out var decimalValue))
90+
{
91+
enumMemberName = "CSharp" + decimalValue.ToString("0.#", CultureInfo.InvariantCulture).Replace('.', '_');
92+
}
93+
94+
parseOptions = parseOptions.WithLanguageVersion(Enum.Parse<LanguageVersion>(enumMemberName, ignoreCase: true));
95+
break;
96+
case "ImplicitUsings":
97+
implicitUsings = ParseFeatureBool(property.Value);
98+
break;
99+
default:
100+
throw new NotImplementedException($"Review whether support is required for <{property.Name}> ({filePath})");
101+
}
102+
}
103+
break;
104+
105+
default:
106+
throw new NotImplementedException($"Review whether support is required for <{element.Name}> ({filePath})");
107+
}
108+
}
109+
110+
if (parseOptions.SpecifiedLanguageVersion == LanguageVersion.Default)
111+
parseOptions = parseOptions.WithLanguageVersion(SupportedTargetFrameworks[targetFramework!].DefaultLanguageVersion);
112+
113+
compilationOptions = compilationOptions.WithWarningLevel(SupportedTargetFrameworks[targetFramework!].DefaultWarningLevel);
114+
115+
var generatedSources = ImmutableArray.CreateBuilder<SyntaxTree>();
116+
117+
if (implicitUsings)
118+
{
119+
generatedSources.Add(SyntaxFactory.ParseSyntaxTree("""
120+
global using System;
121+
global using System.Collections.Generic;
122+
global using System.IO;
123+
global using System.Linq;
124+
global using System.Net.Http;
125+
global using System.Threading;
126+
global using System.Threading.Tasks;
127+
""",
128+
parseOptions,
129+
assemblyName + ".GlobalUsings.g.cs"));
130+
}
131+
132+
return new CsprojParseResult(
133+
assemblyName,
134+
targetFramework!,
135+
parseOptions,
136+
compilationOptions,
137+
generatedSources.DrainToImmutable());
138+
}
139+
140+
private static bool ParseFeatureBool(string propertyValue)
141+
{
142+
return propertyValue.Equals("enable", StringComparison.OrdinalIgnoreCase)
143+
|| propertyValue.Equals("true", StringComparison.OrdinalIgnoreCase);
144+
}
145+
}

tools/ExampleTester/GeneratedExample.cs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
using ExampleExtractor;
1+
using System.Reflection;
2+
using System.Text;
3+
using ExampleExtractor;
24
using Microsoft.Build.Locator;
35
using Microsoft.CodeAnalysis;
46
using Microsoft.CodeAnalysis.MSBuild;
57
using Newtonsoft.Json;
6-
using System.Reflection;
7-
using System.Text;
88
using Utilities;
99

1010
namespace ExampleTester;
@@ -38,18 +38,25 @@ internal async Task<bool> Test(TesterConfiguration configuration, StatusCheckLog
3838
{
3939
logger.ConsoleOnlyLog(Metadata.Source, Metadata.StartLine, Metadata.EndLine, $"Testing {Metadata.Name} from {Metadata.Source}", "ExampleTester");
4040

41-
// Explicitly do a release build, to avoid implicitly defining DEBUG.
42-
var properties = new Dictionary<string, string> { { "Configuration", "Release" } };
43-
using var workspace = MSBuildWorkspace.Create(properties);
4441
// TODO: Validate this more cleanly.
4542
var projectFile = Metadata.Project is string specifiedProject
4643
? Path.Combine(directory, $"{specifiedProject}.csproj")
4744
: Directory.GetFiles(directory, "*.csproj").Single();
48-
var project = await workspace.OpenProjectAsync(projectFile);
49-
var compilation = await project.GetCompilationAsync();
50-
if (compilation is null)
45+
46+
Compilation compilation;
47+
try
5148
{
52-
throw new InvalidOperationException("Project has no Compilation");
49+
compilation = FastCsprojCompilationParser.CreateCompilation(projectFile);
50+
}
51+
catch (NotImplementedException)
52+
{
53+
// Explicitly do a release build, to avoid implicitly defining DEBUG.
54+
var properties = new Dictionary<string, string> { { "Configuration", "Release" } };
55+
using var workspace = MSBuildWorkspace.Create(properties);
56+
57+
var project = await workspace.OpenProjectAsync(projectFile);
58+
compilation = await project.GetCompilationAsync()
59+
?? throw new InvalidOperationException("Project has no Compilation");
5360
}
5461

5562
bool ret = true;

0 commit comments

Comments
 (0)