Skip to content

Commit 2f327b1

Browse files
authored
Reuse ProjectCollection across DTBs (#51620)
1 parent 9ba2e9f commit 2f327b1

File tree

6 files changed

+110
-89
lines changed

6 files changed

+110
-89
lines changed

src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,26 @@ public void WatchFiles(FileWatcher fileWatcher)
3232
fileWatcher.WatchFiles(BuildFiles);
3333
}
3434

35+
public static ImmutableDictionary<string, string> GetGlobalBuildOptions(IEnumerable<string> buildArguments, EnvironmentOptions environmentOptions)
36+
{
37+
// See https://github.com/dotnet/project-system/blob/main/docs/well-known-project-properties.md
38+
39+
return CommandLineOptions.ParseBuildProperties(buildArguments)
40+
.ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value)
41+
.SetItem(PropertyNames.DotNetWatchBuild, "true")
42+
.SetItem(PropertyNames.DesignTimeBuild, "true")
43+
.SetItem(PropertyNames.SkipCompilerExecution, "true")
44+
.SetItem(PropertyNames.ProvideCommandLineArgs, "true")
45+
// F# targets depend on host path variable:
46+
.SetItem("DOTNET_HOST_PATH", environmentOptions.MuxerPath);
47+
}
48+
3549
/// <summary>
3650
/// Loads project graph and performs design-time build.
3751
/// </summary>
3852
public static EvaluationResult? TryCreate(
53+
ProjectGraphFactory factory,
3954
string rootProjectPath,
40-
IEnumerable<string> buildArguments,
4155
ILogger logger,
4256
GlobalOptions options,
4357
EnvironmentOptions environmentOptions,
@@ -46,20 +60,8 @@ public void WatchFiles(FileWatcher fileWatcher)
4660
{
4761
var buildReporter = new BuildReporter(logger, options, environmentOptions);
4862

49-
// See https://github.com/dotnet/project-system/blob/main/docs/well-known-project-properties.md
50-
51-
var globalOptions = CommandLineOptions.ParseBuildProperties(buildArguments)
52-
.ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value)
53-
.SetItem(PropertyNames.DotNetWatchBuild, "true")
54-
.SetItem(PropertyNames.DesignTimeBuild, "true")
55-
.SetItem(PropertyNames.SkipCompilerExecution, "true")
56-
.SetItem(PropertyNames.ProvideCommandLineArgs, "true")
57-
// F# targets depend on host path variable:
58-
.SetItem("DOTNET_HOST_PATH", environmentOptions.MuxerPath);
59-
60-
var projectGraph = ProjectGraphUtilities.TryLoadProjectGraph(
63+
var projectGraph = factory.TryLoadProjectGraph(
6164
rootProjectPath,
62-
globalOptions,
6365
logger,
6466
projectGraphRequired: true,
6567
cancellationToken);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Immutable;
5+
using Microsoft.Build.Evaluation;
6+
using Microsoft.Build.Graph;
7+
using Microsoft.Extensions.Logging;
8+
using ILogger = Microsoft.Extensions.Logging.ILogger;
9+
10+
namespace Microsoft.DotNet.Watch;
11+
12+
internal sealed class ProjectGraphFactory(ImmutableDictionary<string, string> globalOptions)
13+
{
14+
/// <summary>
15+
/// Reuse <see cref="ProjectCollection"/> with XML element caching to improve performance.
16+
///
17+
/// The cache is automatically updated when build files change.
18+
/// https://github.com/dotnet/msbuild/blob/b6f853defccd64ae1e9c7cf140e7e4de68bff07c/src/Build/Definition/ProjectCollection.cs#L343-L354
19+
/// </summary>
20+
private readonly ProjectCollection _collection = new(
21+
globalProperties: globalOptions,
22+
loggers: [],
23+
remoteLoggers: [],
24+
ToolsetDefinitionLocations.Default,
25+
maxNodeCount: 1,
26+
onlyLogCriticalEvents: false,
27+
loadProjectsReadOnly: false,
28+
useAsynchronousLogging: false,
29+
reuseProjectRootElementCache: true);
30+
31+
/// <summary>
32+
/// Tries to create a project graph by running the build evaluation phase on the <see cref="rootProjectFile"/>.
33+
/// </summary>
34+
public ProjectGraph? TryLoadProjectGraph(
35+
string rootProjectFile,
36+
ILogger logger,
37+
bool projectGraphRequired,
38+
CancellationToken cancellationToken)
39+
{
40+
var entryPoint = new ProjectGraphEntryPoint(rootProjectFile, globalOptions);
41+
try
42+
{
43+
return new ProjectGraph([entryPoint], _collection, projectInstanceFactory: null, cancellationToken);
44+
}
45+
catch (Exception e) when (e is not OperationCanceledException)
46+
{
47+
// ProejctGraph aggregates OperationCanceledException exception,
48+
// throw here to propagate the cancellation.
49+
cancellationToken.ThrowIfCancellationRequested();
50+
51+
logger.LogDebug("Failed to load project graph.");
52+
53+
if (e is AggregateException { InnerExceptions: var innerExceptions })
54+
{
55+
foreach (var inner in innerExceptions)
56+
{
57+
Report(inner);
58+
}
59+
}
60+
else
61+
{
62+
Report(e);
63+
}
64+
65+
void Report(Exception e)
66+
{
67+
if (projectGraphRequired)
68+
{
69+
logger.LogError(e.Message);
70+
}
71+
else
72+
{
73+
logger.LogWarning(e.Message);
74+
}
75+
}
76+
}
77+
78+
return null;
79+
}
80+
}

src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,14 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections.Immutable;
5-
using Microsoft.Build.Evaluation;
64
using Microsoft.Build.Execution;
75
using Microsoft.Build.Graph;
86
using Microsoft.DotNet.Cli;
9-
using Microsoft.Extensions.Logging;
10-
using ILogger = Microsoft.Extensions.Logging.ILogger;
117

128
namespace Microsoft.DotNet.Watch;
139

1410
internal static class ProjectGraphUtilities
1511
{
16-
/// <summary>
17-
/// Tries to create a project graph by running the build evaluation phase on the <see cref="rootProjectFile"/>.
18-
/// </summary>
19-
public static ProjectGraph? TryLoadProjectGraph(
20-
string rootProjectFile,
21-
ImmutableDictionary<string, string> globalOptions,
22-
ILogger logger,
23-
bool projectGraphRequired,
24-
CancellationToken cancellationToken)
25-
{
26-
var entryPoint = new ProjectGraphEntryPoint(rootProjectFile, globalOptions);
27-
try
28-
{
29-
// Create a new project collection that does not reuse element cache
30-
// to work around https://github.com/dotnet/msbuild/issues/12064:
31-
var collection = new ProjectCollection(
32-
globalProperties: globalOptions,
33-
loggers: [],
34-
remoteLoggers: [],
35-
ToolsetDefinitionLocations.Default,
36-
maxNodeCount: 1,
37-
onlyLogCriticalEvents: false,
38-
loadProjectsReadOnly: false,
39-
useAsynchronousLogging: false,
40-
reuseProjectRootElementCache: false);
41-
42-
return new ProjectGraph([entryPoint], collection, projectInstanceFactory: null, cancellationToken);
43-
}
44-
catch (Exception e) when (e is not OperationCanceledException)
45-
{
46-
// ProejctGraph aggregates OperationCanceledException exception,
47-
// throw here to propagate the cancellation.
48-
cancellationToken.ThrowIfCancellationRequested();
49-
50-
logger.LogDebug("Failed to load project graph.");
51-
52-
if (e is AggregateException { InnerExceptions: var innerExceptions })
53-
{
54-
foreach (var inner in innerExceptions)
55-
{
56-
Report(inner);
57-
}
58-
}
59-
else
60-
{
61-
Report(e);
62-
}
63-
64-
void Report(Exception e)
65-
{
66-
if (projectGraphRequired)
67-
{
68-
logger.LogError(e.Message);
69-
}
70-
else
71-
{
72-
logger.LogWarning(e.Message);
73-
}
74-
}
75-
}
76-
77-
return null;
78-
}
79-
8012
public static string GetDisplayName(this ProjectGraphNode projectNode)
8113
=> $"{Path.GetFileNameWithoutExtension(projectNode.ProjectInstance.FullPath)} ({projectNode.GetTargetFramework()})";
8214

src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ internal sealed class HotReloadDotNetWatcher
2020
private readonly RestartPrompt? _rudeEditRestartPrompt;
2121

2222
private readonly DotNetWatchContext _context;
23+
private readonly ProjectGraphFactory _designTimeBuildGraphFactory;
2324

2425
internal Task? Test_FileChangesCompletedTask { get; set; }
2526

@@ -40,6 +41,11 @@ public HotReloadDotNetWatcher(DotNetWatchContext context, IConsole console, IRun
4041

4142
_rudeEditRestartPrompt = new RestartPrompt(context.Logger, consoleInput, noPrompt ? true : null);
4243
}
44+
45+
_designTimeBuildGraphFactory = new ProjectGraphFactory(
46+
EvaluationResult.GetGlobalBuildOptions(
47+
context.RootProjectOptions.BuildArguments,
48+
context.EnvironmentOptions));
4349
}
4450

4551
public async Task WatchAsync(CancellationToken shutdownCancellationToken)
@@ -824,8 +830,8 @@ private async ValueTask<EvaluationResult> EvaluateRootProjectAsync(bool restore,
824830
var stopwatch = Stopwatch.StartNew();
825831

826832
var result = EvaluationResult.TryCreate(
827-
_context.RootProjectOptions.ProjectPath,
828-
_context.RootProjectOptions.BuildArguments,
833+
_designTimeBuildGraphFactory,
834+
_context.RootProjectOptions.ProjectPath,
829835
_context.BuildLogger,
830836
_context.Options,
831837
_context.EnvironmentOptions,

src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ internal class MSBuildFileSetFactory(
3030
private EnvironmentOptions EnvironmentOptions => buildReporter.EnvironmentOptions;
3131
private ILogger Logger => buildReporter.Logger;
3232

33+
private readonly ProjectGraphFactory _buildGraphFactory = new(
34+
globalOptions: CommandLineOptions.ParseBuildProperties(buildArguments).ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value));
35+
3336
internal sealed class EvaluationResult(IReadOnlyDictionary<string, FileItem> files, ProjectGraph? projectGraph)
3437
{
3538
public readonly IReadOnlyDictionary<string, FileItem> Files = files;
@@ -124,10 +127,7 @@ void AddFile(string filePath, string? staticWebAssetPath)
124127
ProjectGraph? projectGraph = null;
125128
if (requireProjectGraph != null)
126129
{
127-
var globalOptions = CommandLineOptions.ParseBuildProperties(buildArguments)
128-
.ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value);
129-
130-
projectGraph = ProjectGraphUtilities.TryLoadProjectGraph(rootProjectFile, globalOptions, Logger, requireProjectGraph.Value, cancellationToken);
130+
projectGraph = _buildGraphFactory.TryLoadProjectGraph(rootProjectFile, Logger, requireProjectGraph.Value, cancellationToken);
131131
if (projectGraph == null && requireProjectGraph == true)
132132
{
133133
return null;

test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public async Task ReferenceOutputAssembly_False()
2424
var reporter = new TestReporter(Logger);
2525
var loggerFactory = new LoggerFactory(reporter);
2626
var logger = loggerFactory.CreateLogger("Test");
27-
var projectGraph = ProjectGraphUtilities.TryLoadProjectGraph(options.ProjectPath, globalOptions: [], logger, projectGraphRequired: false, CancellationToken.None);
27+
var factory = new ProjectGraphFactory(globalOptions: []);
28+
var projectGraph = factory.TryLoadProjectGraph(options.ProjectPath, logger, projectGraphRequired: false, CancellationToken.None);
2829
var handler = new CompilationHandler(logger, processRunner);
2930

3031
await handler.Workspace.UpdateProjectConeAsync(hostProject, CancellationToken.None);

0 commit comments

Comments
 (0)