Skip to content

Commit 3f311b6

Browse files
authored
Avoid materializing outputs for unqueried dependencies (#117)
* Avoid materializing outputs for recursively queried nodes * Fix multitargeting
1 parent cdc79ce commit 3f311b6

File tree

4 files changed

+96
-24
lines changed

4 files changed

+96
-24
lines changed

src/Common/Caching/CacheClient.cs

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,10 @@ await AddNodeAsync(
357357

358358
public async Task<(PathSet?, NodeBuildResult?)> GetNodeAsync(
359359
NodeContext nodeContext,
360+
bool materializeOutputs,
360361
CancellationToken cancellationToken)
361362
{
362-
(PathSet? PathSet, NodeBuildResult? NodeBuildResult) result = await GetNodeInternalAsync(nodeContext, cancellationToken);
363+
(PathSet? PathSet, NodeBuildResult? NodeBuildResult) result = await GetNodeInternalAsync(nodeContext, materializeOutputs, cancellationToken);
363364

364365
// On cache miss ensure all dependencies are materialized before returning to MSBuild so that MSBuild's execution will actually work.
365366
if (_enableAsyncMaterialization && result.NodeBuildResult == null)
@@ -378,6 +379,7 @@ await AddNodeAsync(
378379

379380
public async Task<(PathSet?, NodeBuildResult?)> GetNodeInternalAsync(
380381
NodeContext nodeContext,
382+
bool materializeOutputs,
381383
CancellationToken cancellationToken)
382384
{
383385
Context context = new(RootContext);
@@ -498,23 +500,26 @@ async Task PlaceFilesAsync(CancellationToken ct)
498500
}
499501
}
500502

501-
if (_enableAsyncMaterialization)
503+
if (materializeOutputs)
502504
{
503-
_materializationTasks.TryAdd(
504-
nodeContext,
505-
// Avoid using a cancellation token since MSBuild will cancel it when it thinks the build is finished and we await these tasks at that point.
506-
// Note that this means that we effectively cannot cancel this operation once started and the user will have to wait.
507-
Task.Run(
508-
async () =>
509-
{
510-
await PlaceFilesAsync(CancellationToken.None);
511-
_materializationTasks.TryRemove(nodeContext, out _);
512-
},
513-
CancellationToken.None));
514-
}
515-
else
516-
{
517-
await PlaceFilesAsync(cancellationToken);
505+
if (_enableAsyncMaterialization)
506+
{
507+
_materializationTasks.TryAdd(
508+
nodeContext,
509+
// Avoid using a cancellation token since MSBuild will cancel it when it thinks the build is finished and we await these tasks at that point.
510+
// Note that this means that we effectively cannot cancel this operation once started and the user will have to wait.
511+
Task.Run(
512+
async () =>
513+
{
514+
await PlaceFilesAsync(CancellationToken.None);
515+
_materializationTasks.TryRemove(nodeContext, out _);
516+
},
517+
CancellationToken.None));
518+
}
519+
else
520+
{
521+
await PlaceFilesAsync(cancellationToken);
522+
}
518523
}
519524

520525
return (pathSet, nodeBuildResult);

src/Common/Caching/ICacheClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Task<NodeBuildResult> AddNodeAsync(
1919
Func<IReadOnlyDictionary<string, ContentHash>, NodeBuildResult> nodeBuildResultBuilder,
2020
CancellationToken cancellationToken);
2121

22-
Task<(PathSet?, NodeBuildResult?)> GetNodeAsync(NodeContext nodeContext, CancellationToken cancellationToken);
22+
Task<(PathSet?, NodeBuildResult?)> GetNodeAsync(NodeContext nodeContext, bool materializeOutputs, CancellationToken cancellationToken);
2323

2424
Task ShutdownAsync(CancellationToken cancellationToken);
2525
}

src/Common/MSBuildCachePluginBase.cs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ private async Task BeginBuildInnerAsync(CacheContext context, PluginLoggerBase l
334334
if (Settings.GetResultsForUnqueriedDependencies)
335335
{
336336
ConcurrentDictionary<NodeContext, Lazy<Task<CacheResult>>> cacheResults = new(concurrencyLevel: Environment.ProcessorCount, _nodeContexts.Count);
337-
_getCacheResultAsync = (nodeContext, logger, cancellationToken) => GetCacheResultRecursivelyAsync(cacheResults, nodeContext, logger, cancellationToken);
337+
_getCacheResultAsync = (nodeContext, logger, cancellationToken) => GetCacheResultRecursivelyAsync(cacheResults, nodeContext, materializeOutputs: true, logger, cancellationToken);
338338
}
339339
else
340340
{
@@ -405,6 +405,7 @@ private async Task<CacheResult> GetCacheResultInnerAsync(BuildRequestData buildR
405405
private async Task<CacheResult> GetCacheResultRecursivelyAsync(
406406
ConcurrentDictionary<NodeContext, Lazy<Task<CacheResult>>> cacheResults,
407407
NodeContext nodeContext,
408+
bool materializeOutputs,
408409
PluginLoggerBase logger,
409410
CancellationToken cancellationToken)
410411
{
@@ -413,12 +414,21 @@ private async Task<CacheResult> GetCacheResultRecursivelyAsync(
413414
return await cacheResults.GetOrAdd(nodeContext, new Lazy<Task<CacheResult>>(
414415
async () =>
415416
{
417+
bool isOuterBuild = nodeContext.ProjectInstance.IsOuterBuild();
418+
416419
foreach (NodeContext dependency in nodeContext.Dependencies)
417420
{
418421
if (dependency.BuildResult == null)
419422
{
423+
// When querying recursively, avoid materializing the outputs. That node was never directly queried, so its outputs
424+
// are not desired from the caller. Note that there is an assumption that the node won't be queried directly later
425+
// as that would break the expected "bottom-up" build order of graph builds.
426+
// Special-case the outer build of a multitargeting project which dependencies on the inner builds for which we do
427+
// want the outputs.
428+
bool materializeOutputs = isOuterBuild && dependency.ProjectInstance.IsInnerBuild();
429+
420430
logger.LogMessage($"Querying cache for missing build result for dependency '{dependency.Id}'");
421-
CacheResult dependencyResult = await GetCacheResultRecursivelyAsync(cacheResults, dependency, logger, cancellationToken);
431+
CacheResult dependencyResult = await GetCacheResultRecursivelyAsync(cacheResults, dependency, materializeOutputs, logger, cancellationToken);
422432
logger.LogMessage($"Dependency '{dependency.Id}' cache result: '{dependencyResult.ResultType}'");
423433

424434
if (dependencyResult.ResultType != CacheResultType.CacheHit)
@@ -430,7 +440,7 @@ private async Task<CacheResult> GetCacheResultRecursivelyAsync(
430440
}
431441
}
432442

433-
return await GetCacheResultSingleAsync(nodeContext, logger, cancellationToken);
443+
return await GetCacheResultSingleAsync(nodeContext, materializeOutputs, logger, cancellationToken);
434444
})).Value;
435445
}
436446

@@ -446,10 +456,10 @@ private async Task<CacheResult> GetCacheResultNonRecursiveAsync(NodeContext node
446456
}
447457
}
448458

449-
return await GetCacheResultSingleAsync(nodeContext, logger, cancellationToken);
459+
return await GetCacheResultSingleAsync(nodeContext, materializeOutputs: true, logger, cancellationToken);
450460
}
451461

452-
private async Task<CacheResult> GetCacheResultSingleAsync(NodeContext nodeContext, PluginLoggerBase logger, CancellationToken cancellationToken)
462+
private async Task<CacheResult> GetCacheResultSingleAsync(NodeContext nodeContext, bool materializeOutputs, PluginLoggerBase logger, CancellationToken cancellationToken)
453463
{
454464
if (!Initialized)
455465
{
@@ -458,7 +468,7 @@ private async Task<CacheResult> GetCacheResultSingleAsync(NodeContext nodeContex
458468

459469
nodeContext.SetStartTime();
460470

461-
(PathSet? pathSet, NodeBuildResult? nodeBuildResult) = await _cacheClient.GetNodeAsync(nodeContext, cancellationToken);
471+
(PathSet? pathSet, NodeBuildResult? nodeBuildResult) = await _cacheClient.GetNodeAsync(nodeContext, materializeOutputs, cancellationToken);
462472
if (nodeBuildResult is null)
463473
{
464474
Interlocked.Increment(ref _cacheMissCount);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using Microsoft.Build.Execution;
6+
7+
namespace Microsoft.MSBuildCache;
8+
9+
internal static class ProjectInstanceExtensions
10+
{
11+
/// <summary>
12+
/// Parses a property that may contain a bool, returning the default value if the property is not
13+
/// set, or returns false if the property is set to 'false', otherwise returning true.
14+
/// </summary>
15+
public static bool GetBoolPropertyValue(this ProjectInstance projectInstance, string propName, bool defaultValue = false)
16+
{
17+
string prop = projectInstance.GetPropertyValue(propName);
18+
if (string.IsNullOrWhiteSpace(prop))
19+
{
20+
return defaultValue;
21+
}
22+
23+
if (string.Equals(prop, "false", StringComparison.OrdinalIgnoreCase))
24+
{
25+
return false;
26+
}
27+
28+
return true;
29+
}
30+
31+
public static bool IsPlatformNegotiationBuild(this ProjectInstance projectInstance)
32+
{
33+
// the fact that there is zero global properties here allows us to determine this was an evaluation done for the sole use of determining a referenced project's compatible platforms. We know that this is a build for this use
34+
// because "IsGraphBuild" will be set to true in the global properties of all other project instances.
35+
return projectInstance.IsPlatformNegotiationEnabled() && projectInstance.GlobalProperties.Count == 0;
36+
}
37+
38+
public static bool IsPlatformNegotiationEnabled(this ProjectInstance projectInstance)
39+
{
40+
// Determine whether or not the platform negotiation is turned on in msbuild
41+
return !string.IsNullOrWhiteSpace(projectInstance.GetPropertyValue("EnableDynamicPlatformResolution"));
42+
}
43+
44+
public static bool IsInnerBuild(this ProjectInstance projectInstance)
45+
{
46+
// This follows the logic of MSBuild's ProjectInterpretation.GetProjectType.
47+
// See: https://github.com/microsoft/msbuild/blob/master/src/Build/Graph/ProjectInterpretation.cs
48+
return !projectInstance.IsPlatformNegotiationBuild() && !string.IsNullOrWhiteSpace(projectInstance.GetPropertyValue(projectInstance.GetPropertyValue("InnerBuildProperty"))) && !string.IsNullOrWhiteSpace(projectInstance.GetPropertyValue(projectInstance.GetPropertyValue("InnerBuildPropertyValues")));
49+
}
50+
51+
public static bool IsOuterBuild(this ProjectInstance projectInstance)
52+
{
53+
// This follows the logic of MSBuild's ProjectInterpretation.GetProjectType.
54+
// See: https://github.com/microsoft/msbuild/blob/master/src/Build/Graph/ProjectInterpretation.cs
55+
return !projectInstance.IsPlatformNegotiationBuild() && !projectInstance.IsInnerBuild() && !string.IsNullOrWhiteSpace(projectInstance.GetPropertyValue(projectInstance.GetPropertyValue("InnerBuildPropertyValues")));
56+
}
57+
}

0 commit comments

Comments
 (0)