Skip to content

Commit 84e8d21

Browse files
authored
Dump the fingerprint if the targets at least match for read scenarios (#124)
1 parent a2d1da7 commit 84e8d21

File tree

2 files changed

+72
-27
lines changed

2 files changed

+72
-27
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ These settings are common across all plugins, although different implementations
7272
| `$(MSBuildCacheAllowProcessCloseAfterProjectFinishProcessPatterns)` | `Glob[]` | `\**\mspdbsrv.exe` | Processes to allow to exit after the project which launched it completes, ie detached processes. |
7373
| `$(MSBuildCacheGlobalPropertiesToIgnore)` | `string[]` | `CurrentSolutionConfigurationContents; ShouldUnsetParentConfigurationAndPlatform; BuildingInsideVisualStudio; BuildingSolutionFile; SolutionDir; SolutionExt; SolutionFileName; SolutionName; SolutionPath; _MSDeployUserAgent`, as well as all proeprties related to plugin settings | The list of global properties to exclude from consideration by the cache |
7474
| `$(MSBuildCacheGetResultsForUnqueriedDependencies)` | `bool` | false | Whether to try and query the cache for dependencies if they have not previously been requested. This option can help in cases where the build isn't done in graph order, or if some projects are skipped. |
75-
| `$(MSBuildCacheTargetsToIgnore)` | `string[]` | `GetTargetFrameworks;GetNativeManifest;GetCopyToOutputDirectoryItems;GetTargetFrameworksWithPlatformForSingleTargetFramework` | The list of targets to ignore when determining if a build request matches a cache entry. This is intended for "information gathering" targets which do not have side-effect. eg. a build with `/t:Build` and `/t:Build;GetTargetFrameworks` should be considered to have equivalent results. Note: This only works "one-way" in that the build request is allowed to have missing targets, while the cache entry is not. This is to avoid a situation where a build request recieves a cache hit with missing target results, where a cache hit with extra target results is acceptable. |
75+
| `$(MSBuildCacheTargetsToIgnore)` | `string[]` | `GetTargetFrameworks;GetNativeManifest;GetCopyToOutputDirectoryItems;GetTargetFrameworksWithPlatformForSingleTargetFramework` | The list of targets to ignore when determining if a build request matches a cache entry. This is intended for "information gathering" targets which do not have side-effect. eg. a build with `/t:Build` and `/t:Build;GetTargetFrameworks` should be considered to have equivalent results. Note: This only works "one-way" in that the build request is allowed to have missing targets, while the cache entry is not. This is to avoid a situation where a build request receives a cache hit with missing target results, where a cache hit with extra target results is acceptable. |
7676
| `$(MSBuildCacheSkipUnchangedOutputFiles)` | `bool` | false | Whether to avoid writing output files on cache hit if the file is unchanged, which can improve performance for incremental builds. A file is considered unchanged if it exists, the previously placed file and file to be placed have the same hash, and the the previously placed file and current file on disk have the same timestamp and file size. |
7777
| `$(MSBuildCacheTouchOutputFiles)` | `bool` | false | Whether to update the last write time for output files on cache hit. All files for a given cache entry will have the same timestamp. Note that outputs which skip materialization via `MSBuildCacheSkipUnchangedOutputFiles` are still touched. |
7878
| `$(MSBuildCacheIgnoreDotNetSdkPatchVersion)` | `bool` | false | Whether to ignore the patch version when doing cache lookups. This trades off some correctness for the sake of getting cache hits when the SDK version isn't exactly the same. The default behavior is to consider the exact SDK version, eg. "8.0.404". With this setting set to true, it will instead use something like "8.0.4XX". Note that the major version, minor version, and feature bands are still considered. |

src/Common/MSBuildCachePluginBase.cs

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ private async Task<CacheResult> GetCacheResultInnerAsync(BuildRequestData buildR
393393
return CacheResult.IndicateNonCacheHit(CacheResultType.CacheNotApplicable);
394394
}
395395

396-
if (!DoTargetsMatch(nodeContext.TargetNames, buildRequest.TargetNames))
396+
if (!DoTargetsMatchForRead(nodeContext.TargetNames, buildRequest.TargetNames))
397397
{
398398
logger.LogMessage($"`TargetNames` does not match for {nodeContext.Id}. `{string.Join(";", nodeContext.TargetNames)}` vs `{string.Join(";", buildRequest.TargetNames)}`.");
399399
return CacheResult.IndicateNonCacheHit(CacheResultType.CacheNotApplicable);
@@ -497,7 +497,12 @@ private void HandleFileAccessInner(FileAccessContext fileAccessContext, FileAcce
497497
_hasHadFileAccessReport = true;
498498

499499
NodeContext? nodeContext = GetNodeContext(fileAccessContext);
500-
if (nodeContext == null)
500+
if (nodeContext is null)
501+
{
502+
return;
503+
}
504+
505+
if (!DoTargetsMatchForWrite(nodeContext.TargetNames, fileAccessContext.Targets))
501506
{
502507
return;
503508
}
@@ -516,7 +521,12 @@ public override void HandleProcess(FileAccessContext fileAccessContext, ProcessD
516521
private void HandleProcessInner(FileAccessContext fileAccessContext, ProcessData processData)
517522
{
518523
NodeContext? nodeContext = GetNodeContext(fileAccessContext);
519-
if (nodeContext == null)
524+
if (nodeContext is null)
525+
{
526+
return;
527+
}
528+
529+
if (!DoTargetsMatchForWrite(nodeContext.TargetNames, fileAccessContext.Targets))
520530
{
521531
return;
522532
}
@@ -539,11 +549,46 @@ private async Task HandleProjectFinishedInnerAsync(FileAccessContext fileAccessC
539549
}
540550

541551
NodeContext? nodeContext = GetNodeContext(fileAccessContext);
542-
if (nodeContext == null)
552+
if (nodeContext is null)
543553
{
544554
return;
545555
}
546556

557+
// If the target do not match for write, the result is not sufficient to cache as it's missing targets.
558+
if (!DoTargetsMatchForWrite(nodeContext.TargetNames, fileAccessContext.Targets))
559+
{
560+
// Remove the initial targets, if any, from the reported targets to guess at the originally requested targets.
561+
IReadOnlyList<string> requestedTargets;
562+
if (nodeContext.ProjectInstance.InitialTargets.Count > 0)
563+
{
564+
List<string> requestedTargetsList = new(fileAccessContext.Targets.Count - nodeContext.ProjectInstance.InitialTargets.Count);
565+
foreach (string reportedTarget in fileAccessContext.Targets)
566+
{
567+
if (!nodeContext.ProjectInstance.InitialTargets.Contains(reportedTarget, StringComparer.OrdinalIgnoreCase))
568+
{
569+
requestedTargetsList.Add(reportedTarget);
570+
}
571+
}
572+
573+
requestedTargets = requestedTargetsList;
574+
}
575+
else
576+
{
577+
requestedTargets = fileAccessContext.Targets;
578+
}
579+
580+
// If the targets match for read, it is very unlikely that we will later get a result that does match for write as the
581+
// targets ignored for reads should be information-gathering only. To help debug cache misses dump the fingerprint.
582+
if (DoTargetsMatchForRead(nodeContext.TargetNames, requestedTargets))
583+
{
584+
// Because we do not have the file accesses, we cannot know the PathSet.
585+
// This means that any cache miss analysis will need to take this into account.
586+
await DumpFingerprintLogAsync(logger, nodeContext, null);
587+
}
588+
589+
return;
590+
}
591+
547592
nodeContext.SetEndTime();
548593

549594
// In niche cases, eg traversal projects, the build may be successful despite a dependency failing. Ignore these cases since we can't properly fingerprint failed dependencies.
@@ -556,18 +601,14 @@ private async Task HandleProjectFinishedInnerAsync(FileAccessContext fileAccessC
556601
}
557602
}
558603

559-
FileAccesses fileAccesses = _fileAccessRepository.FinishProject(nodeContext);
560-
561604
// If file access reports are disabled in MSBuild we can't cache anything as we don't know what to cache.
562605
if (!_hasHadFileAccessReport)
563606
{
564-
// We still want to dump the fingerprint to help debug cache misses.
565-
// However, note that because we do not have the file accesses, we cannot know the PathSet.
566-
// This means that any cache miss analysis will need to take this into account.
567-
await DumpFingerprintLogAsync(logger, nodeContext, null);
568607
return;
569608
}
570609

610+
FileAccesses fileAccesses = _fileAccessRepository.FinishProject(nodeContext);
611+
571612
// Package files are commonly just copied as outputs, so track the package inputs to compare with package outputs to avoid caching them.
572613
Dictionary<ContentHash, string> hashesToPackageFiles = new();
573614
List<Task<(byte[]?, string)>> packageFileHashingTasks = new();
@@ -743,22 +784,10 @@ await Task.WhenAll(
743784
}
744785

745786
NodeDescriptor nodeDescriptor = _nodeDescriptorFactory.Create(fileAccessContext.ProjectFullPath, fileAccessContext.GlobalProperties);
746-
if (!_nodeContexts.TryGetValue(nodeDescriptor, out NodeContext? nodeContext))
747-
{
748-
return null;
749-
}
750-
751-
// Note: Checking if the targets we expect is a subset of the targets we got. InitialTargets in particular may cause extra targets to be executed.
752-
// We will end up caching these extra results, but this is also intended as they do end up executing with the original request.
753-
if (!nodeContext.TargetNames.IsSubsetOf(fileAccessContext.Targets))
754-
{
755-
return null;
756-
}
757-
758-
return nodeContext;
787+
return _nodeContexts.TryGetValue(nodeDescriptor, out NodeContext? nodeContext) ? nodeContext : null;
759788
}
760789

761-
private bool DoTargetsMatch(HashSet<string> expectedTargets, IEnumerable<string> requestedTargets)
790+
private bool DoTargetsMatchForRead(HashSet<string> expectedTargets, IEnumerable<string> requestedTargets)
762791
{
763792
if (Settings is null || Settings.TargetsToIgnore.Count == 0)
764793
{
@@ -781,6 +810,13 @@ private bool DoTargetsMatch(HashSet<string> expectedTargets, IEnumerable<string>
781810
return extraTargets.Count == 0;
782811
}
783812

813+
private static bool DoTargetsMatchForWrite(HashSet<string> expectedTargets, IEnumerable<string> reportedTargets)
814+
{
815+
// Check if the targets we expect is a subset of the targets we got. InitialTargets in particular may cause extra targets to be executed.
816+
// We will end up caching these extra results, but this is also intended as they do end up executing with the original request.
817+
return expectedTargets.IsSubsetOf(reportedTargets);
818+
}
819+
784820
private async Task DumpNodeContextsAsync(PluginLoggerBase logger, Dictionary<NodeDescriptor, NodeContext> nodeContexts)
785821
{
786822
if (_nodeContexts is null)
@@ -899,15 +935,24 @@ private async Task DumpFingerprintLogAsync(
899935
return;
900936
}
901937

938+
Fingerprint? weakFingerprint = await FingerprintFactory.GetWeakFingerprintAsync(nodeContext);
939+
if (weakFingerprint is null)
940+
{
941+
// Nothing to dump. Likely dependencies failed or cache missed.
942+
return;
943+
}
944+
945+
Fingerprint? strongFingerprint = await FingerprintFactory.GetStrongFingerprintAsync(pathSet);
946+
902947
string filePath = Path.Combine(nodeContext.LogDirectory, "fingerprint.json");
903948
try
904949
{
905950
using FileStream fileStream = File.Create(filePath);
906951
await using var jsonWriter = new Utf8JsonWriter(fileStream, SerializationHelper.WriterOptions);
907952

908953
jsonWriter.WriteStartObject();
909-
WriteFingerprintJson(jsonWriter, "weak", await FingerprintFactory.GetWeakFingerprintAsync(nodeContext));
910-
WriteFingerprintJson(jsonWriter, "strong", await FingerprintFactory.GetStrongFingerprintAsync(pathSet));
954+
WriteFingerprintJson(jsonWriter, "weak", weakFingerprint);
955+
WriteFingerprintJson(jsonWriter, "strong", strongFingerprint);
911956
jsonWriter.WriteEndObject();
912957
}
913958
catch (Exception ex)

0 commit comments

Comments
 (0)