Skip to content

Commit 462a917

Browse files
Merge pull request #8726 from dfederm/project-cache-vnext
This change add "cache add" functionality to project caching. Today project caching exposes a hook to plugins for "I'm about to build this thing, do you want to take over instead?" and that's it. This change adds a few more hooks which report file accesses and processes (via Detours) and also for when a project finishes building. This allows a plugin to collect the file accesses and add entries to the cache for future replayability.
2 parents 5958b59 + a67e9ac commit 462a917

File tree

73 files changed

+2607
-51
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+2607
-51
lines changed

NuGet.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<add key="dotnet6" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json" />
99
<add key="dotnet8" value="https://dnceng.pkgs.visualstudio.com/public/_packaging/dotnet8/nuget/v3/index.json" />
1010
<add key="dotnet8-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8-transport/nuget/v3/index.json" />
11+
<add key="BuildXL" value="https://pkgs.dev.azure.com/ms/BuildXL/_packaging/BuildXL/nuget/v3/index.json" />
1112
</packageSources>
1213
<disabledPackageSources />
1314
</configuration>

eng/dependabot/Packages.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
<PackageVersion Include="LargeAddressAware" Version="1.0.5" />
2020
<PackageVersion Update="LargeAddressAware" Condition="'$(LargeAddressAwareVersion)' != ''" Version="$(LargeAddressAwareVersion)" />
2121

22+
<PackageVersion Include="Microsoft.BuildXL.Processes" Version="0.1.0-20230727.4.2" />
23+
<PackageVersion Update="Microsoft.BuildXL.Processes" Condition="'$(BuildXLProcessesVersion)' != ''" Version="$(BuildXLProcessesVersion)" />
24+
2225
<PackageVersion Include="Microsoft.VisualStudio.Setup.Configuration.Interop" Version="3.2.2146" PrivateAssets="All" />
2326
<PackageVersion Update="Microsoft.VisualStudio.Setup.Configuration.Interop" Condition="'$(MicrosoftVisualStudioSetupConfigurationInteropVersion)' != ''" Version="$(MicrosoftVisualStudioSetupConfigurationInteropVersion)" PrivateAssets="All" />
2427

src/Build.UnitTests/BackEnd/TaskHostTaskComplete_Tests.cs

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using Microsoft.Build.BackEnd;
77
using Microsoft.Build.Framework;
8+
using Microsoft.Build.Framework.FileAccess;
89
using Microsoft.Build.Shared;
910
using Microsoft.Build.Utilities;
1011
using Xunit;
@@ -25,21 +26,67 @@ public class TaskHostTaskComplete_Tests
2526
[Fact]
2627
public void TestConstructors()
2728
{
28-
TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success), null);
29-
TaskHostTaskComplete complete2 = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Failure), null);
30-
TaskHostTaskComplete complete3 = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedDuringInitialization, new ArgumentOutOfRangeException()), null);
31-
TaskHostTaskComplete complete4 = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedDuringExecution, new ArgumentNullException()), null);
29+
#if FEATURE_REPORTFILEACCESSES
30+
var fileAccessData = new List<FileAccessData>()
31+
{
32+
new FileAccessData(
33+
ReportedFileOperation.CreateFile,
34+
RequestedAccess.Read,
35+
0,
36+
0,
37+
DesiredAccess.GENERIC_READ,
38+
FlagsAndAttributes.FILE_ATTRIBUTE_NORMAL,
39+
"foo",
40+
null,
41+
true),
42+
};
43+
#endif
44+
45+
_ = new TaskHostTaskComplete(
46+
new OutOfProcTaskHostTaskResult(TaskCompleteType.Success),
47+
#if FEATURE_REPORTFILEACCESSES
48+
fileAccessData,
49+
#endif
50+
null);
51+
_ = new TaskHostTaskComplete(
52+
new OutOfProcTaskHostTaskResult(TaskCompleteType.Failure),
53+
#if FEATURE_REPORTFILEACCESSES
54+
fileAccessData,
55+
#endif
56+
null);
57+
_ = new TaskHostTaskComplete(
58+
new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedDuringInitialization,
59+
new ArgumentOutOfRangeException()),
60+
#if FEATURE_REPORTFILEACCESSES
61+
fileAccessData,
62+
#endif
63+
null);
64+
_ = new TaskHostTaskComplete(
65+
new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedDuringExecution, new ArgumentNullException()),
66+
#if FEATURE_REPORTFILEACCESSES
67+
fileAccessData,
68+
#endif
69+
null);
3270

3371
IDictionary<string, object> parameters = new Dictionary<string, object>();
34-
TaskHostTaskComplete complete5 = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters), null);
72+
_ = new TaskHostTaskComplete(
73+
new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters),
74+
#if FEATURE_REPORTFILEACCESSES
75+
null,
76+
#endif
77+
null);
3578

3679
IDictionary<string, object> parameters2 = new Dictionary<string, object>();
3780
parameters2.Add("Text", "Hello!");
3881
parameters2.Add("MyBoolValue", true);
3982
parameters2.Add("MyITaskItem", new TaskItem("ABC"));
4083
parameters2.Add("ItemArray", new ITaskItem[] { new TaskItem("DEF"), new TaskItem("GHI"), new TaskItem("JKL") });
41-
42-
TaskHostTaskComplete complete6 = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters2), null);
84+
_ = new TaskHostTaskComplete(
85+
new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters2),
86+
#if FEATURE_REPORTFILEACCESSES
87+
null,
88+
#endif
89+
null);
4390
}
4491

4592
/// <summary>
@@ -60,7 +107,12 @@ public void TestInvalidConstructors()
60107
[Fact]
61108
public void TestTranslationWithNullDictionary()
62109
{
63-
TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success), null);
110+
TaskHostTaskComplete complete = new(
111+
new OutOfProcTaskHostTaskResult(TaskCompleteType.Success),
112+
#if FEATURE_REPORTFILEACCESSES
113+
null,
114+
#endif
115+
null);
64116

65117
((ITranslatable)complete).Translate(TranslationHelpers.GetWriteTranslator());
66118
INodePacket packet = TaskHostTaskComplete.FactoryForDeserialization(TranslationHelpers.GetReadTranslator());
@@ -78,7 +130,12 @@ public void TestTranslationWithNullDictionary()
78130
[Fact]
79131
public void TestTranslationWithEmptyDictionary()
80132
{
81-
TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, new Dictionary<string, object>()), null);
133+
TaskHostTaskComplete complete = new(
134+
new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, new Dictionary<string, object>()),
135+
#if FEATURE_REPORTFILEACCESSES
136+
null,
137+
#endif
138+
null);
82139

83140
((ITranslatable)complete).Translate(TranslationHelpers.GetWriteTranslator());
84141
INodePacket packet = TaskHostTaskComplete.FactoryForDeserialization(TranslationHelpers.GetReadTranslator());
@@ -99,7 +156,12 @@ public void TestTranslationWithValueTypesInDictionary()
99156
IDictionary<string, object> parameters = new Dictionary<string, object>();
100157
parameters.Add("Text", "Foo");
101158
parameters.Add("BoolValue", false);
102-
TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters), null);
159+
TaskHostTaskComplete complete = new(
160+
new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters),
161+
#if FEATURE_REPORTFILEACCESSES
162+
null,
163+
#endif
164+
null);
103165

104166
((ITranslatable)complete).Translate(TranslationHelpers.GetWriteTranslator());
105167
INodePacket packet = TaskHostTaskComplete.FactoryForDeserialization(TranslationHelpers.GetReadTranslator());
@@ -121,7 +183,12 @@ public void TestTranslationWithITaskItemInDictionary()
121183
{
122184
IDictionary<string, object> parameters = new Dictionary<string, object>();
123185
parameters.Add("TaskItemValue", new TaskItem("Foo"));
124-
TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters), null);
186+
TaskHostTaskComplete complete = new(
187+
new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters),
188+
#if FEATURE_REPORTFILEACCESSES
189+
null,
190+
#endif
191+
null);
125192

126193
((ITranslatable)complete).Translate(TranslationHelpers.GetWriteTranslator());
127194
INodePacket packet = TaskHostTaskComplete.FactoryForDeserialization(TranslationHelpers.GetReadTranslator());
@@ -142,7 +209,12 @@ public void TestTranslationWithITaskItemArrayInDictionary()
142209
{
143210
IDictionary<string, object> parameters = new Dictionary<string, object>();
144211
parameters.Add("TaskItemArrayValue", new ITaskItem[] { new TaskItem("Foo"), new TaskItem("Baz") });
145-
TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters), null);
212+
TaskHostTaskComplete complete = new(
213+
new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters),
214+
#if FEATURE_REPORTFILEACCESSES
215+
null,
216+
#endif
217+
null);
146218

147219
((ITranslatable)complete).Translate(TranslationHelpers.GetWriteTranslator());
148220
INodePacket packet = TaskHostTaskComplete.FactoryForDeserialization(TranslationHelpers.GetReadTranslator());
@@ -168,7 +240,12 @@ private void AssertInvalidConstructorThrows(Type expectedExceptionType, TaskComp
168240

169241
try
170242
{
171-
TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(taskResult, taskOutputParameters, taskException, taskExceptionMessage, taskExceptionMessageArgs), buildProcessEnvironment);
243+
TaskHostTaskComplete complete = new(
244+
new OutOfProcTaskHostTaskResult(taskResult, taskOutputParameters, taskException, taskExceptionMessage, taskExceptionMessageArgs),
245+
#if FEATURE_REPORTFILEACCESSES
246+
null,
247+
#endif
248+
buildProcessEnvironment);
172249
}
173250
catch (Exception e)
174251
{

src/Build/BackEnd/BuildManager/BuildManager.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
using Microsoft.Build.Exceptions;
2626
using Microsoft.Build.Experimental;
2727
using Microsoft.Build.Experimental.ProjectCache;
28+
using Microsoft.Build.FileAccesses;
2829
using Microsoft.Build.Framework;
2930
using Microsoft.Build.Framework.Telemetry;
3031
using Microsoft.Build.Graph;
@@ -558,6 +559,19 @@ public void BeginBuild(BuildParameters parameters)
558559
_buildParameters.OutputResultsCacheFile = FileUtilities.NormalizePath("msbuild-cache");
559560
}
560561

562+
#if FEATURE_REPORTFILEACCESSES
563+
if (_buildParameters.ReportFileAccesses)
564+
{
565+
// To properly report file access, we need to disable the in-proc node which won't be detoured.
566+
_buildParameters.DisableInProcNode = true;
567+
568+
// Node reuse must be disabled as future builds will not be able to listen to events raised by detours.
569+
_buildParameters.EnableNodeReuse = false;
570+
571+
_componentFactories.ReplaceFactory(BuildComponentType.NodeLauncher, DetouredNodeLauncher.CreateComponent);
572+
}
573+
#endif
574+
561575
// Initialize components.
562576
_nodeManager = ((IBuildComponentHost)this).GetComponent(BuildComponentType.NodeManager) as INodeManager;
563577

@@ -572,9 +586,17 @@ public void BeginBuild(BuildParameters parameters)
572586

573587
InitializeCaches();
574588

589+
#if FEATURE_REPORTFILEACCESSES
590+
var fileAccessManager = ((IBuildComponentHost)this).GetComponent(BuildComponentType.FileAccessManager) as IFileAccessManager;
591+
#endif
592+
575593
_projectCacheService = new ProjectCacheService(
576594
this,
577595
loggingService,
596+
#if FEATURE_REPORTFILEACCESSES
597+
fileAccessManager,
598+
#endif
599+
_configCache,
578600
_buildParameters.ProjectCacheDescriptor);
579601

580602
_taskHostNodeManager = ((IBuildComponentHost)this).GetComponent(BuildComponentType.TaskHostNodeManager) as INodeManager;
@@ -584,7 +606,9 @@ public void BeginBuild(BuildParameters parameters)
584606
_nodeManager.RegisterPacketHandler(NodePacketType.BuildRequestConfiguration, BuildRequestConfiguration.FactoryForDeserialization, this);
585607
_nodeManager.RegisterPacketHandler(NodePacketType.BuildRequestConfigurationResponse, BuildRequestConfigurationResponse.FactoryForDeserialization, this);
586608
_nodeManager.RegisterPacketHandler(NodePacketType.BuildResult, BuildResult.FactoryForDeserialization, this);
609+
_nodeManager.RegisterPacketHandler(NodePacketType.FileAccessReport, FileAccessReport.FactoryForDeserialization, this);
587610
_nodeManager.RegisterPacketHandler(NodePacketType.NodeShutdown, NodeShutdown.FactoryForDeserialization, this);
611+
_nodeManager.RegisterPacketHandler(NodePacketType.ProcessReport, ProcessReport.FactoryForDeserialization, this);
588612
_nodeManager.RegisterPacketHandler(NodePacketType.ResolveSdkRequest, SdkResolverRequest.FactoryForDeserialization, SdkResolverService as INodePacketHandler);
589613
_nodeManager.RegisterPacketHandler(NodePacketType.ResourceRequest, ResourceRequest.FactoryForDeserialization, this);
590614

@@ -1564,6 +1588,16 @@ private void ProcessPacket(int node, INodePacket packet)
15641588
HandleNodeShutdown(node, shutdownPacket);
15651589
break;
15661590

1591+
case NodePacketType.FileAccessReport:
1592+
FileAccessReport fileAccessReport = ExpectPacketType<FileAccessReport>(packet, NodePacketType.FileAccessReport);
1593+
HandleFileAccessReport(node, fileAccessReport);
1594+
break;
1595+
1596+
case NodePacketType.ProcessReport:
1597+
ProcessReport processReport = ExpectPacketType<ProcessReport>(packet, NodePacketType.ProcessReport);
1598+
HandleProcessReport(node, processReport);
1599+
break;
1600+
15671601
default:
15681602
ErrorUtilities.ThrowInternalError("Unexpected packet received by BuildManager: {0}", packet.Type);
15691603
break;
@@ -2371,6 +2405,39 @@ private void HandleResult(int node, BuildResult result)
23712405
configuration.ProjectTargets ??= result.ProjectTargets;
23722406
}
23732407

2408+
// Only report results to the project cache services if it's the result for a build submission.
2409+
// Note that graph builds create a submission for each node in the graph, so each node in the graph will be
2410+
// handled here. This intentionally mirrors the behavior for cache requests, as it doesn't make sense to
2411+
// report for projects which aren't going to be requested. Ideally, *any* request could be handled, but that
2412+
// would require moving the cache service interactions to the Scheduler.
2413+
if (_buildSubmissions.TryGetValue(result.SubmissionId, out BuildSubmission buildSubmission))
2414+
{
2415+
// The result may be associated with the build submission due to it being the submission which
2416+
// caused the build, but not the actual request which was originally used with the build submission.
2417+
// ie. it may be a dependency of the "root-level" project which is associated with this submission, which
2418+
// isn't what we're looking for. Ensure only the actual submission's request is considered.
2419+
if (buildSubmission.BuildRequest != null
2420+
&& buildSubmission.BuildRequest.ConfigurationId == configuration.ConfigurationId
2421+
&& _projectCacheService.ShouldUseCache(configuration))
2422+
{
2423+
BuildEventContext buildEventContext = _projectStartedEvents.TryGetValue(result.SubmissionId, out BuildEventArgs buildEventArgs)
2424+
? buildEventArgs.BuildEventContext
2425+
: new BuildEventContext(result.SubmissionId, node, configuration.Project?.EvaluationId ?? BuildEventContext.InvalidEvaluationId, configuration.ConfigurationId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId);
2426+
try
2427+
{
2428+
_projectCacheService.HandleBuildResultAsync(configuration, result, buildEventContext, _executionCancellationTokenSource.Token).Wait();
2429+
}
2430+
catch (AggregateException ex) when (ex.InnerExceptions.All(inner => inner is OperationCanceledException))
2431+
{
2432+
// The build is being cancelled. Swallow any exceptions related specifically to cancellation.
2433+
}
2434+
catch (OperationCanceledException)
2435+
{
2436+
// The build is being cancelled. Swallow any exceptions related specifically to cancellation.
2437+
}
2438+
}
2439+
}
2440+
23742441
IEnumerable<ScheduleResponse> response = _scheduler.ReportResult(node, result);
23752442
PerformSchedulingActions(response);
23762443
}
@@ -2437,6 +2504,36 @@ private void HandleNodeShutdown(int node, NodeShutdown shutdownPacket)
24372504
CheckForActiveNodesAndCleanUpSubmissions();
24382505
}
24392506

2507+
/// <summary>
2508+
/// Report the received <paramref name="fileAccessReport"/> to the file access manager.
2509+
/// </summary>
2510+
/// <param name="nodeId">The id of the node from which the <paramref name="fileAccessReport"/> was received.</param>
2511+
/// <param name="fileAccessReport">The file access report.</param>
2512+
private void HandleFileAccessReport(int nodeId, FileAccessReport fileAccessReport)
2513+
{
2514+
#if FEATURE_REPORTFILEACCESSES
2515+
if (_buildParameters.ReportFileAccesses)
2516+
{
2517+
((FileAccessManager)((IBuildComponentHost)this).GetComponent(BuildComponentType.FileAccessManager)).ReportFileAccess(fileAccessReport.FileAccessData, nodeId);
2518+
}
2519+
#endif
2520+
}
2521+
2522+
/// <summary>
2523+
/// Report the received <paramref name="processReport"/> to the file access manager.
2524+
/// </summary>
2525+
/// <param name="nodeId">The id of the node from which the <paramref name="processReport"/> was received.</param>
2526+
/// <param name="processReport">The process data report.</param>
2527+
private void HandleProcessReport(int nodeId, ProcessReport processReport)
2528+
{
2529+
#if FEATURE_REPORTFILEACCESSES
2530+
if (_buildParameters.ReportFileAccesses)
2531+
{
2532+
((FileAccessManager)((IBuildComponentHost)this).GetComponent(BuildComponentType.FileAccessManager)).ReportProcess(processReport.ProcessData, nodeId);
2533+
}
2534+
#endif
2535+
}
2536+
24402537
/// <summary>
24412538
/// If there are no more active nodes, cleans up any remaining submissions.
24422539
/// </summary>

0 commit comments

Comments
 (0)