зеркало из https://github.com/microsoft/BuildXL.git
Merged PR 523144: Improve cache miss analysis presentation
This is the first phase of giving more love to cache miss analysis: improving the analysis presentation. The current presentation has the following issues: - Analysis outputs are hard to understand, and look cryptic. - Cache miss analysis relies on a buggy third-party algorithm that can spit out a lot of irrelevant outputs. - Analysis of weak fingerprint do not respect order-independent entries. - Etc. The new presentation is still Json based and is similar to (but more refined than) the legacy cache miss analysis (which customers like). Example of new presentation (for mismatched strong fingerprints): ``` { "StrongFingerprint": { "Old": "471383773F3C19EAC1F4EF4DDD496E2076428406", "New": "FA15D762D7BFE52570BDFC070917B4103E0EE713" }, "Paths": { "Changed": { "b:\\out\\objects\\7\\0\\907noclwl174l942adk4eeeedjrh2a\\t_1\\fingerprints73945ac\\0\\obj\\readonly\\src_0": { "Old": "AccessType: DirectoryEnumeration | Hash: 3100D2E3F4", "New": "AccessType: DirectoryEnumeration | Hash: C675A28412", "Members": { "Added": [ "src_3", "src_4" ] } } } } } ``` The old presentation is preserved because some customer (namely Eric) relies on the old format. Related work items: #1520132, #1567602, #1572558
This commit is contained in:
Родитель
66cc175a00
Коммит
dbbcb47695
|
@ -28,12 +28,15 @@ The "analysis.txt" file in the output directory shows the first pip in each depe
|
||||||
|
|
||||||
### Diff Format
|
### Diff Format
|
||||||
|
|
||||||
Both cache miss analyzers use *JsonDiffPatch* to diff *WeakFingerprint* and *StrongFingerprint* json files. If you are not familiar with json diff syntax, you can find the reference in the following links:
|
The new cache miss analyzer produces diff outputs in the form of Json. The new cache miss analyzer offers two different diff formats. The first format, called *CustomJsonDiff*, is a custom diff format resulting from our own diff algorithm that understands the semantics of weak and strong fingerprints.
|
||||||
|
|
||||||
|
The second diff format is *JsonDiffPatch*. This format shows the diff as a delta between two Json reprsentations. To output this format, BuildXL relies on an external diff algorithm. References about the algorithm and the diff syntaxk can be found in the following links:
|
||||||
|
|
||||||
[General diff syntax reference](https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md)
|
[General diff syntax reference](https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md)
|
||||||
|
|
||||||
[Array diff syntax reference](https://github.com/benjamine/jsondiffpatch/blob/master/docs/arrays.md)
|
[Array diff syntax reference](https://github.com/benjamine/jsondiffpatch/blob/master/docs/arrays.md)
|
||||||
|
|
||||||
|
The default diff format is CustomJsonDiff. One can specifying explicitly the diff format to use by using `/cacheMissDiffFormat:<CustomJsonDiff|JsonDiffPatch>`
|
||||||
|
|
||||||
#### Known Limitations
|
#### Known Limitations
|
||||||
The cache miss analyzer works correctly under the assumption that the two builds being compared shared the same graph scope and processed all of the same pips through the full scheduling algorithm. When this assumption is false, the analyzer may produce the following messages:
|
The cache miss analyzer works correctly under the assumption that the two builds being compared shared the same graph scope and processed all of the same pips through the full scheduling algorithm. When this assumption is false, the analyzer may produce the following messages:
|
||||||
|
|
|
@ -251,6 +251,9 @@ namespace BuildXL
|
||||||
OptionHandlerFactory.CreateBoolOptionWithValue(
|
OptionHandlerFactory.CreateBoolOptionWithValue(
|
||||||
"cacheMiss",
|
"cacheMiss",
|
||||||
(opt, sign) => ParseCacheMissAnalysisOption(opt, sign, loggingConfiguration, pathTable)),
|
(opt, sign) => ParseCacheMissAnalysisOption(opt, sign, loggingConfiguration, pathTable)),
|
||||||
|
OptionHandlerFactory.CreateOption(
|
||||||
|
"cacheMissDiffFormat",
|
||||||
|
opt => CommandLineUtilities.ParseEnumOption<CacheMissDiffFormat>(opt)),
|
||||||
OptionHandlerFactory.CreateOption(
|
OptionHandlerFactory.CreateOption(
|
||||||
"cacheSessionName",
|
"cacheSessionName",
|
||||||
opt => cacheConfiguration.CacheSessionName = CommandLineUtilities.ParseStringOption(opt)),
|
opt => cacheConfiguration.CacheSessionName = CommandLineUtilities.ParseStringOption(opt)),
|
||||||
|
|
|
@ -358,6 +358,11 @@ namespace BuildXL
|
||||||
Strings.HelpText_DisplayHelp_CacheMiss,
|
Strings.HelpText_DisplayHelp_CacheMiss,
|
||||||
HelpLevel.Verbose);
|
HelpLevel.Verbose);
|
||||||
|
|
||||||
|
hw.WriteOption(
|
||||||
|
"/cacheMissDiffFormat:[format]",
|
||||||
|
Strings.HelpText_DisplayHelp_CacheMissDiffFormat,
|
||||||
|
HelpLevel.Verbose);
|
||||||
|
|
||||||
hw.WriteOption(
|
hw.WriteOption(
|
||||||
"/scriptShowSlowest[+|-]",
|
"/scriptShowSlowest[+|-]",
|
||||||
Strings.HelpText_DisplayHelp_ScriptShowSlowest,
|
Strings.HelpText_DisplayHelp_ScriptShowSlowest,
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<root>
|
<root>
|
||||||
<!--
|
<!--
|
||||||
Microsoft ResX Schema
|
Microsoft ResX Schema
|
||||||
|
|
||||||
Version 2.0
|
Version 2.0
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
The primary goals of this format is to allow a simple XML format
|
||||||
that is mostly human readable. The generation and parsing of the
|
that is mostly human readable. The generation and parsing of the
|
||||||
various data types are done through the TypeConverter classes
|
various data types are done through the TypeConverter classes
|
||||||
associated with the data types.
|
associated with the data types.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
... ado.net/XML headers & schema ...
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
<resheader name="version">2.0</resheader>
|
<resheader name="version">2.0</resheader>
|
||||||
|
@ -26,36 +26,36 @@
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
<comment>This is a comment</comment>
|
<comment>This is a comment</comment>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
There are any number of "resheader" rows that contain simple
|
||||||
name/value pairs.
|
name/value pairs.
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
Each data row contains a name, and value. The row also contains a
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
text/value conversion through the TypeConverter architecture.
|
text/value conversion through the TypeConverter architecture.
|
||||||
Classes that don't support this are serialized and stored with the
|
Classes that don't support this are serialized and stored with the
|
||||||
mimetype set.
|
mimetype set.
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
The mimetype is used for serialized objects, and tells the
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
read any of the formats listed below.
|
read any of the formats listed below.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
value : The object must be serialized with
|
value : The object must be serialized with
|
||||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
value : The object must be serialized with
|
value : The object must be serialized with
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
value : The object must be serialized into a byte array
|
value : The object must be serialized into a byte array
|
||||||
: using a System.ComponentModel.TypeConverter
|
: using a System.ComponentModel.TypeConverter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
-->
|
-->
|
||||||
|
@ -1033,4 +1033,7 @@ Example: ad2d42d2ec5d2ca0c0b7ad65402d07c7ef40b91e</value>
|
||||||
<data name="HelpText_DisplayHelp_ValidateCgManifest" xml:space="preserve">
|
<data name="HelpText_DisplayHelp_ValidateCgManifest" xml:space="preserve">
|
||||||
<value>Validates the cgmanifest.json file at the specified path. This file should contain up-to-date names and versions of all Nuget packages used within BuildXL for Component Governance. Any mismatch will cause the Build to fail. Updated file can be created using the /generateCgManifestForNugets:<path></value>
|
<value>Validates the cgmanifest.json file at the specified path. This file should contain up-to-date names and versions of all Nuget packages used within BuildXL for Component Governance. Any mismatch will cause the Build to fail. Updated file can be created using the /generateCgManifestForNugets:<path></value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="HelpText_DisplayHelp_CacheMissDiffFormat" xml:space="preserve">
|
||||||
|
<value>Diff format for cache miss analysis. Allowed values are CustomJsonDiff and JsonPatchDiff. Defaults to CustomJsonDiff</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
|
@ -68,14 +68,14 @@ namespace BuildXL.Engine.Cache.Serialization
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.Values.Count != other.Values.Count)
|
if (Values.Count != other.Values.Count)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < this.Values.Count; ++i)
|
for (int i = 0; i < Values.Count; ++i)
|
||||||
{
|
{
|
||||||
if (this.Values[i] != other.Values[i])
|
if (Values[i] != other.Values[i])
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ namespace BuildXL.Engine.Cache.Serialization
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class JsonTree
|
public static class JsonTree
|
||||||
{
|
{
|
||||||
private static JsonDiffPatch s_jdp = null;
|
private static readonly JsonDiffPatch s_jdp = null;
|
||||||
|
|
||||||
static JsonTree()
|
static JsonTree()
|
||||||
{
|
{
|
||||||
|
@ -156,7 +156,7 @@ namespace BuildXL.Engine.Cache.Serialization
|
||||||
Parent = parentNode,
|
Parent = parentNode,
|
||||||
Name = reader.Value.ToString()
|
Name = reader.Value.ToString()
|
||||||
};
|
};
|
||||||
parentNode.Children.AddFirst(currentNode);
|
parentNode.Children.AddLast(currentNode);
|
||||||
break;
|
break;
|
||||||
case JsonToken.String:
|
case JsonToken.String:
|
||||||
currentNode.Values.Add(reader.Value.ToString());
|
currentNode.Values.Add(reader.Value.ToString());
|
||||||
|
@ -191,7 +191,7 @@ namespace BuildXL.Engine.Cache.Serialization
|
||||||
// If the root is being used to just point to a bunch of child nodes, skip printing it
|
// If the root is being used to just point to a bunch of child nodes, skip printing it
|
||||||
if (string.IsNullOrEmpty(root.Name))
|
if (string.IsNullOrEmpty(root.Name))
|
||||||
{
|
{
|
||||||
for (var it = root.Children.Last; it != null; it = it.Previous)
|
for (var it = root.Children.First; it != null; it = it.Next)
|
||||||
{
|
{
|
||||||
BuildStringHelper(it.Value, wr);
|
BuildStringHelper(it.Value, wr);
|
||||||
}
|
}
|
||||||
|
@ -230,7 +230,7 @@ namespace BuildXL.Engine.Cache.Serialization
|
||||||
collectionWriter.Add(value);
|
collectionWriter.Add(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var it = n.Children.Last; it != null; it = it.Previous)
|
for (var it = n.Children.First; it != null; it = it.Next)
|
||||||
{
|
{
|
||||||
BuildStringHelper(it.Value, collectionWriter);
|
BuildStringHelper(it.Value, collectionWriter);
|
||||||
}
|
}
|
||||||
|
@ -384,5 +384,21 @@ namespace BuildXL.Engine.Cache.Serialization
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Visits tree.
|
||||||
|
/// </summary>
|
||||||
|
public static void VisitTree(JsonNode root, Action<JsonNode> processNode, bool recurse)
|
||||||
|
{
|
||||||
|
for (var it = root.Children.First; it != null; it = it.Next)
|
||||||
|
{
|
||||||
|
JsonNode node = it.Value;
|
||||||
|
processNode(node);
|
||||||
|
if (recurse)
|
||||||
|
{
|
||||||
|
VisitTree(node, processNode, recurse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,27 @@ namespace BuildXL.Scheduler.Fingerprints
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")]
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")]
|
||||||
public readonly struct ObservedPathSet
|
public readonly struct ObservedPathSet
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constants used for labeling.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct Labels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Label for <see cref="ObservedPathSet.UnsafeOptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
public const string UnsafeOptions = nameof(ObservedPathSet.UnsafeOptions);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Label for <see cref="ObservedPathSet.ObservedAccessedFileNames"/>.
|
||||||
|
/// </summary>
|
||||||
|
public const string ObservedAccessedFileNames = nameof(ObservedPathSet.ObservedAccessedFileNames);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Label for <see cref="Paths"/>.
|
||||||
|
/// </summary>
|
||||||
|
public const string Paths = nameof(ObservedPathSet.Paths);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Failure describing why deserialization of a path set failed (<see cref="ObservedPathSet.TryDeserialize"/>).
|
/// Failure describing why deserialization of a path set failed (<see cref="ObservedPathSet.TryDeserialize"/>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
|
||||||
|
namespace BuildXL.Scheduler.Fingerprints
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Struct naming common fields in pip fingerprint.
|
||||||
|
/// </summary>
|
||||||
|
public struct PipFingerprintField
|
||||||
|
{
|
||||||
|
/// <nodoc/>
|
||||||
|
public const string ExecutionAndFingerprintOptionsHash = nameof(ExecutionAndFingerprintOptionsHash);
|
||||||
|
|
||||||
|
/// <nodoc/>
|
||||||
|
public const string ContentHashAlgorithmName = nameof(ContentHashAlgorithmName);
|
||||||
|
|
||||||
|
/// <nodoc/>
|
||||||
|
public const string PipType = nameof(PipType);
|
||||||
|
|
||||||
|
/// <nodoc/>
|
||||||
|
public struct Process
|
||||||
|
{
|
||||||
|
/// <nodoc/>
|
||||||
|
public const string SourceChangeAffectedInputList = nameof(SourceChangeAffectedInputList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <nodoc/>
|
||||||
|
public struct FileDependency
|
||||||
|
{
|
||||||
|
/// <nodoc/>
|
||||||
|
public const string PathNormalizedWriteFileContent = nameof(PathNormalizedWriteFileContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <nodoc/>
|
||||||
|
public struct FileOutput
|
||||||
|
{
|
||||||
|
/// <nodoc/>
|
||||||
|
public const string Attributes = nameof(Attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -174,12 +174,12 @@ namespace BuildXL.Scheduler.Fingerprints
|
||||||
Contract.Requires(fingerprinter != null);
|
Contract.Requires(fingerprinter != null);
|
||||||
Contract.Requires(pip != null);
|
Contract.Requires(pip != null);
|
||||||
|
|
||||||
fingerprinter.Add("ExecutionAndFingerprintOptionsHash", m_extraFingerprintSalts.CalculatedSaltsFingerprint);
|
fingerprinter.Add(PipFingerprintField.ExecutionAndFingerprintOptionsHash, m_extraFingerprintSalts.CalculatedSaltsFingerprint);
|
||||||
|
|
||||||
// Fingerprints must change when outputs are hashed with a different algorithm.
|
// Fingerprints must change when outputs are hashed with a different algorithm.
|
||||||
fingerprinter.Add("ContentHashAlgorithmName", s_outputContentHashAlgorithmName);
|
fingerprinter.Add(PipFingerprintField.ContentHashAlgorithmName, s_outputContentHashAlgorithmName);
|
||||||
|
|
||||||
fingerprinter.Add("PipType", GetHashMarker(pip));
|
fingerprinter.Add(PipFingerprintField.PipType, GetHashMarker(pip));
|
||||||
|
|
||||||
switch (pip.PipType)
|
switch (pip.PipType)
|
||||||
{
|
{
|
||||||
|
@ -212,7 +212,7 @@ namespace BuildXL.Scheduler.Fingerprints
|
||||||
Contract.Requires(fingerprinter != null);
|
Contract.Requires(fingerprinter != null);
|
||||||
Contract.Requires(hashSourceFile != null);
|
Contract.Requires(hashSourceFile != null);
|
||||||
|
|
||||||
fingerprinter.Add("File", hashSourceFile.Artifact);
|
fingerprinter.Add(nameof(HashSourceFile.Artifact), hashSourceFile.Artifact);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -223,8 +223,8 @@ namespace BuildXL.Scheduler.Fingerprints
|
||||||
Contract.Requires(fingerprinter != null);
|
Contract.Requires(fingerprinter != null);
|
||||||
Contract.Requires(copyFile != null);
|
Contract.Requires(copyFile != null);
|
||||||
|
|
||||||
AddFileDependency(fingerprinter, "Source", copyFile.Source);
|
AddFileDependency(fingerprinter, nameof(CopyFile.Source), copyFile.Source);
|
||||||
AddFileOutput(fingerprinter, "Destination", copyFile.Destination);
|
AddFileOutput(fingerprinter, nameof(CopyFile.Destination), copyFile.Destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -235,9 +235,9 @@ namespace BuildXL.Scheduler.Fingerprints
|
||||||
Contract.Requires(fingerprinter != null);
|
Contract.Requires(fingerprinter != null);
|
||||||
Contract.Requires(writeFile != null);
|
Contract.Requires(writeFile != null);
|
||||||
|
|
||||||
AddFileOutput(fingerprinter, "Destination", writeFile.Destination);
|
AddFileOutput(fingerprinter, nameof(WriteFile.Destination), writeFile.Destination);
|
||||||
AddPipData(fingerprinter, "Contents", writeFile.Contents);
|
AddPipData(fingerprinter, nameof(WriteFile.Contents), writeFile.Contents);
|
||||||
fingerprinter.Add("Encoding", (byte)writeFile.Encoding);
|
fingerprinter.Add(nameof(WriteFile.Encoding), (byte)writeFile.Encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -248,17 +248,17 @@ namespace BuildXL.Scheduler.Fingerprints
|
||||||
Contract.Requires(fingerprinter != null);
|
Contract.Requires(fingerprinter != null);
|
||||||
Contract.Requires(sealDirectory != null);
|
Contract.Requires(sealDirectory != null);
|
||||||
|
|
||||||
fingerprinter.Add("Path", sealDirectory.DirectoryRoot);
|
fingerprinter.Add(nameof(SealDirectory.DirectoryRoot), sealDirectory.DirectoryRoot);
|
||||||
fingerprinter.Add("Kind", sealDirectory.Kind.ToString());
|
fingerprinter.Add(nameof(SealDirectory.Kind), sealDirectory.Kind.ToString());
|
||||||
fingerprinter.Add("Scrub", sealDirectory.Scrub.ToString());
|
fingerprinter.Add(nameof(SealDirectory.Scrub), sealDirectory.Scrub.ToString());
|
||||||
|
|
||||||
// Sort the contents based on their members' expanded paths so that they are stable across different path tables.
|
// Sort the contents based on their members' expanded paths so that they are stable across different path tables.
|
||||||
var sortedContents = SortedReadOnlyArray<FileArtifact, ExpandedPathFileArtifactComparer>.CloneAndSort(sealDirectory.Contents, m_expandedPathFileArtifactComparer);
|
var sortedContents = SortedReadOnlyArray<FileArtifact, ExpandedPathFileArtifactComparer>.CloneAndSort(sealDirectory.Contents, m_expandedPathFileArtifactComparer);
|
||||||
|
|
||||||
fingerprinter.AddCollection<FileArtifact, ReadOnlyArray<FileArtifact>>("Contents", sortedContents, (fp, f) => AddFileDependency(fp, f));
|
fingerprinter.AddCollection<FileArtifact, ReadOnlyArray<FileArtifact>>(nameof(SealDirectory.Contents), sortedContents, (fp, f) => AddFileDependency(fp, f));
|
||||||
fingerprinter.AddCollection<StringId, ReadOnlyArray<StringId>>("Patterns", sealDirectory.Patterns, (fp, p) => fp.Add(p));
|
fingerprinter.AddCollection<StringId, ReadOnlyArray<StringId>>(nameof(SealDirectory.Patterns), sealDirectory.Patterns, (fp, p) => fp.Add(p));
|
||||||
fingerprinter.Add("IsComposite", sealDirectory.IsComposite.ToString());
|
fingerprinter.Add(nameof(SealDirectory.IsComposite), sealDirectory.IsComposite.ToString());
|
||||||
fingerprinter.AddCollection<DirectoryArtifact, IReadOnlyList<DirectoryArtifact>>("ComposedDirectories", sealDirectory.ComposedDirectories, (fp, d) => AddDirectoryDependency(fp, d));
|
fingerprinter.AddCollection<DirectoryArtifact, IReadOnlyList<DirectoryArtifact>>(nameof(SealDirectory.ComposedDirectories), sealDirectory.ComposedDirectories, (fp, d) => AddDirectoryDependency(fp, d));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -266,62 +266,62 @@ namespace BuildXL.Scheduler.Fingerprints
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void AddWeakFingerprint(IFingerprinter fingerprinter, Process process)
|
protected virtual void AddWeakFingerprint(IFingerprinter fingerprinter, Process process)
|
||||||
{
|
{
|
||||||
fingerprinter.Add("Executable", process.Executable);
|
fingerprinter.Add(nameof(Process.Executable), process.Executable);
|
||||||
fingerprinter.Add("WorkingDirectory", process.WorkingDirectory);
|
fingerprinter.Add(nameof(Process.WorkingDirectory), process.WorkingDirectory);
|
||||||
|
|
||||||
if (process.StandardInput.IsData)
|
if (process.StandardInput.IsData)
|
||||||
{
|
{
|
||||||
// We only add standard input if it is data. If it is a file, then it is guaranteed to be in the dependency list.
|
// We only add standard input if it is data. If it is a file, then it is guaranteed to be in the dependency list.
|
||||||
AddPipData(fingerprinter, "StandardInputData", process.StandardInput.Data);
|
AddPipData(fingerprinter, nameof(Process.StandardInputData), process.StandardInput.Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddFileOutput(fingerprinter, "StandardError", process.StandardError);
|
AddFileOutput(fingerprinter, nameof(Process.StandardError), process.StandardError);
|
||||||
AddFileOutput(fingerprinter, "StandardOutput", process.StandardOutput);
|
AddFileOutput(fingerprinter, nameof(Process.StandardOutput), process.StandardOutput);
|
||||||
|
|
||||||
// Files within untrackedPaths and untrackedScopes are irrelevent to the weak fingerprint and are removed from the fingerprint
|
// Files within untrackedPaths and untrackedScopes are irrelevent to the weak fingerprint and are removed from the fingerprint
|
||||||
ReadOnlyArray<FileArtifact> relevantDependencies = process.Dependencies.Where(d => !IsUntracked(process, d.Path)).ToReadOnlyArray<FileArtifact>();
|
ReadOnlyArray<FileArtifact> relevantDependencies = process.Dependencies.Where(d => !IsUntracked(process, d.Path)).ToReadOnlyArray<FileArtifact>();
|
||||||
|
|
||||||
fingerprinter.AddOrderIndependentCollection<FileArtifact, ReadOnlyArray<FileArtifact>>("Dependencies", relevantDependencies, (fp, f) => AddFileDependency(fp, f), m_expandedPathFileArtifactComparer);
|
fingerprinter.AddOrderIndependentCollection<FileArtifact, ReadOnlyArray<FileArtifact>>(nameof(Process.Dependencies), relevantDependencies, (fp, f) => AddFileDependency(fp, f), m_expandedPathFileArtifactComparer);
|
||||||
fingerprinter.AddOrderIndependentCollection<DirectoryArtifact, ReadOnlyArray<DirectoryArtifact>>("DirectoryDependencies", process.DirectoryDependencies, (fp, d) => AddDirectoryDependency(fp, d), DirectoryComparer);
|
fingerprinter.AddOrderIndependentCollection<DirectoryArtifact, ReadOnlyArray<DirectoryArtifact>>(nameof(Process.DirectoryDependencies), process.DirectoryDependencies, (fp, d) => AddDirectoryDependency(fp, d), DirectoryComparer);
|
||||||
|
|
||||||
fingerprinter.AddOrderIndependentCollection<FileArtifactWithAttributes, ReadOnlyArray<FileArtifactWithAttributes>>("Outputs", process.FileOutputs, (fp, f) => AddFileOutput(fp, f), m_expandedPathFileArtifactWithAttributesComparer);
|
fingerprinter.AddOrderIndependentCollection<FileArtifactWithAttributes, ReadOnlyArray<FileArtifactWithAttributes>>(nameof(Process.FileOutputs), process.FileOutputs, (fp, f) => AddFileOutput(fp, f), m_expandedPathFileArtifactWithAttributesComparer);
|
||||||
fingerprinter.AddOrderIndependentCollection<DirectoryArtifact, ReadOnlyArray<DirectoryArtifact>>("DirectoryOutputs", process.DirectoryOutputs, (h, p) => h.Add(p.Path), DirectoryComparer);
|
fingerprinter.AddOrderIndependentCollection<DirectoryArtifact, ReadOnlyArray<DirectoryArtifact>>(nameof(Process.DirectoryOutputs), process.DirectoryOutputs, (h, p) => h.Add(p.Path), DirectoryComparer);
|
||||||
|
|
||||||
fingerprinter.AddOrderIndependentCollection<AbsolutePath, ReadOnlyArray<AbsolutePath>>("UntrackedPaths", process.UntrackedPaths, (h, p) => h.Add(p), m_pathTable.ExpandedPathComparer);
|
fingerprinter.AddOrderIndependentCollection<AbsolutePath, ReadOnlyArray<AbsolutePath>>(nameof(Process.UntrackedPaths), process.UntrackedPaths, (h, p) => h.Add(p), m_pathTable.ExpandedPathComparer);
|
||||||
fingerprinter.AddOrderIndependentCollection<AbsolutePath, ReadOnlyArray<AbsolutePath>>("UntrackedScopes", process.UntrackedScopes, (h, p) => h.Add(p), m_pathTable.ExpandedPathComparer);
|
fingerprinter.AddOrderIndependentCollection<AbsolutePath, ReadOnlyArray<AbsolutePath>>(nameof(Process.UntrackedScopes), process.UntrackedScopes, (h, p) => h.Add(p), m_pathTable.ExpandedPathComparer);
|
||||||
|
|
||||||
fingerprinter.AddOrderIndependentCollection<AbsolutePath, ReadOnlyArray<AbsolutePath>>("PreserveOutputWhitelist", process.PreserveOutputWhitelist, (h, p) => h.Add(p), m_pathTable.ExpandedPathComparer);
|
fingerprinter.AddOrderIndependentCollection<AbsolutePath, ReadOnlyArray<AbsolutePath>>(nameof(Process.PreserveOutputWhitelist), process.PreserveOutputWhitelist, (h, p) => h.Add(p), m_pathTable.ExpandedPathComparer);
|
||||||
|
|
||||||
fingerprinter.Add("HasUntrackedChildProcesses", process.HasUntrackedChildProcesses ? 1 : 0);
|
fingerprinter.Add(nameof(Process.HasUntrackedChildProcesses), process.HasUntrackedChildProcesses ? 1 : 0);
|
||||||
fingerprinter.Add("AllowUndeclaredSourceReads", process.AllowUndeclaredSourceReads ? 1 : 0);
|
fingerprinter.Add(nameof(Process.AllowUndeclaredSourceReads), process.AllowUndeclaredSourceReads ? 1 : 0);
|
||||||
fingerprinter.Add("AbsentPathProbeUnderOpaquesMode", (byte)process.ProcessAbsentPathProbeInUndeclaredOpaquesMode);
|
fingerprinter.Add(nameof(Process.ProcessAbsentPathProbeInUndeclaredOpaquesMode), (byte)process.ProcessAbsentPathProbeInUndeclaredOpaquesMode);
|
||||||
|
|
||||||
// When DisableCacheLookup is set, the pip is marked as perpetually dirty for incremental scheduling.
|
// When DisableCacheLookup is set, the pip is marked as perpetually dirty for incremental scheduling.
|
||||||
// It must also go to the weak fingerprint so IS will get a miss when you change from the DisableCacheLookup = false
|
// It must also go to the weak fingerprint so IS will get a miss when you change from the DisableCacheLookup = false
|
||||||
// to DisableCacheLookup = true.
|
// to DisableCacheLookup = true.
|
||||||
if (process.DisableCacheLookup)
|
if (process.DisableCacheLookup)
|
||||||
{
|
{
|
||||||
fingerprinter.Add("DisableCacheLookup", ContentHashingUtilities.CreateRandom());
|
fingerprinter.Add(nameof(Process.DisableCacheLookup), ContentHashingUtilities.CreateRandom());
|
||||||
}
|
}
|
||||||
|
|
||||||
fingerprinter.Add("DoubleWritePolicy", (byte)process.DoubleWritePolicy);
|
fingerprinter.Add(nameof(Process.DoubleWritePolicy), (byte)process.DoubleWritePolicy);
|
||||||
|
|
||||||
if (process.RequiresAdmin)
|
if (process.RequiresAdmin)
|
||||||
{
|
{
|
||||||
fingerprinter.Add("RequiresAdmin", 1);
|
fingerprinter.Add(nameof(Process.RequiresAdmin), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fingerprinter.Add("NeedsToRunInContainer", process.NeedsToRunInContainer ? 1 : 0);
|
fingerprinter.Add(nameof(Process.NeedsToRunInContainer), process.NeedsToRunInContainer ? 1 : 0);
|
||||||
fingerprinter.Add("ContainerIsolationLevel", (byte)process.ContainerIsolationLevel);
|
fingerprinter.Add(nameof(Process.ContainerIsolationLevel), (byte)process.ContainerIsolationLevel);
|
||||||
|
|
||||||
AddPipData(fingerprinter, "Arguments", process.Arguments);
|
AddPipData(fingerprinter, nameof(Process.Arguments), process.Arguments);
|
||||||
if (process.ResponseFileData.IsValid)
|
if (process.ResponseFileData.IsValid)
|
||||||
{
|
{
|
||||||
AddPipData(fingerprinter, "ResponseFileData", process.ResponseFileData);
|
AddPipData(fingerprinter, nameof(Process.ResponseFileData), process.ResponseFileData);
|
||||||
}
|
}
|
||||||
|
|
||||||
fingerprinter.AddOrderIndependentCollection<EnvironmentVariable, ReadOnlyArray<EnvironmentVariable>>(
|
fingerprinter.AddOrderIndependentCollection<EnvironmentVariable, ReadOnlyArray<EnvironmentVariable>>(
|
||||||
"EnvironmentVariables",
|
nameof(Process.EnvironmentVariables),
|
||||||
process.EnvironmentVariables,
|
process.EnvironmentVariables,
|
||||||
(fCollection, env) =>
|
(fCollection, env) =>
|
||||||
{
|
{
|
||||||
|
@ -337,33 +337,37 @@ namespace BuildXL.Scheduler.Fingerprints
|
||||||
m_environmentVariableComparer
|
m_environmentVariableComparer
|
||||||
);
|
);
|
||||||
|
|
||||||
fingerprinter.Add("WarningTimeout", process.WarningTimeout.HasValue ? process.WarningTimeout.Value.Ticks : -1);
|
fingerprinter.Add(nameof(Process.WarningTimeout), process.WarningTimeout.HasValue ? process.WarningTimeout.Value.Ticks : -1);
|
||||||
fingerprinter.Add("Timeout", process.Timeout.HasValue ? process.Timeout.Value.Ticks : -1);
|
fingerprinter.Add(nameof(Process.Timeout), process.Timeout.HasValue ? process.Timeout.Value.Ticks : -1);
|
||||||
|
|
||||||
if (process.WarningRegex.IsValid)
|
if (process.WarningRegex.IsValid)
|
||||||
{
|
{
|
||||||
fingerprinter.Add("WarningRegex.Pattern", process.WarningRegex.Pattern);
|
fingerprinter.Add(nameof(Process.WarningRegexPattern), process.WarningRegex.Pattern);
|
||||||
fingerprinter.Add("WarningRegex.Options", (int)process.WarningRegex.Options);
|
fingerprinter.Add(nameof(Process.WarningRegexOptions), (int)process.WarningRegex.Options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.ErrorRegex.IsValid)
|
if (process.ErrorRegex.IsValid)
|
||||||
{
|
{
|
||||||
fingerprinter.Add("ErrorRegex.Pattern", process.ErrorRegex.Pattern);
|
fingerprinter.Add(nameof(Process.ErrorRegexPattern), process.ErrorRegex.Pattern);
|
||||||
fingerprinter.Add("ErrorRegex.Options", (int)process.ErrorRegex.Options);
|
fingerprinter.Add(nameof(Process.ErrorRegexOptions), (int)process.ErrorRegex.Options);
|
||||||
}
|
}
|
||||||
|
|
||||||
fingerprinter.AddCollection<int, ReadOnlyArray<int>>("SuccessExitCodes", process.SuccessExitCodes, (h, i) => h.Add(i));
|
fingerprinter.AddOrderIndependentCollection<int, ReadOnlyArray<int>>(nameof(Process.SuccessExitCodes), process.SuccessExitCodes, (h, i) => h.Add(i), Comparer<int>.Default);
|
||||||
|
|
||||||
if (process.ChangeAffectedInputListWrittenFile.IsValid)
|
if (process.ChangeAffectedInputListWrittenFile.IsValid)
|
||||||
{
|
{
|
||||||
fingerprinter.AddOrderIndependentCollection<AbsolutePath, ReadOnlyArray<AbsolutePath>>("SourceChangeAffectedInputList", m_sourceChangeAffectedInputsLookup(process).ToReadOnlyArray(), (h, p) => h.Add(p), m_pathTable.ExpandedPathComparer);
|
fingerprinter.AddOrderIndependentCollection<AbsolutePath, ReadOnlyArray<AbsolutePath>>(
|
||||||
fingerprinter.Add("ChangeAffectedInputListWrittenFile", process.ChangeAffectedInputListWrittenFile);
|
PipFingerprintField.Process.SourceChangeAffectedInputList,
|
||||||
|
m_sourceChangeAffectedInputsLookup(process).ToReadOnlyArray(),
|
||||||
|
(h, p) => h.Add(p),
|
||||||
|
m_pathTable.ExpandedPathComparer);
|
||||||
|
fingerprinter.Add(nameof(Process.ChangeAffectedInputListWrittenFile), process.ChangeAffectedInputListWrittenFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.ChildProcessesToBreakawayFromSandbox != null)
|
if (process.ChildProcessesToBreakawayFromSandbox != null)
|
||||||
{
|
{
|
||||||
fingerprinter.AddOrderIndependentCollection<StringId, ReadOnlyArray<StringId>>(
|
fingerprinter.AddOrderIndependentCollection<StringId, ReadOnlyArray<StringId>>(
|
||||||
"ChildProcessesToBreakawayFromSandbox",
|
nameof(Process.ChildProcessesToBreakawayFromSandbox),
|
||||||
process.ChildProcessesToBreakawayFromSandbox.Select(processName => processName.StringId).ToReadOnlyArray(),
|
process.ChildProcessesToBreakawayFromSandbox.Select(processName => processName.StringId).ToReadOnlyArray(),
|
||||||
(h, p) => h.Add(p),
|
(h, p) => h.Add(p),
|
||||||
m_pathTable.StringTable.OrdinalComparer);
|
m_pathTable.StringTable.OrdinalComparer);
|
||||||
|
@ -409,7 +413,7 @@ namespace BuildXL.Scheduler.Fingerprints
|
||||||
// into write file outputs so we could get their "path normalized" content(ie with paths tokenized).
|
// into write file outputs so we could get their "path normalized" content(ie with paths tokenized).
|
||||||
fingerprinter.AddNested(
|
fingerprinter.AddNested(
|
||||||
fileArtifact.Path,
|
fileArtifact.Path,
|
||||||
fp => AddPipData(fp, "PathNormalizedWriteFileContent", filePipData));
|
fp => AddPipData(fp, PipFingerprintField.FileDependency.PathNormalizedWriteFileContent, filePipData));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -460,7 +464,7 @@ namespace BuildXL.Scheduler.Fingerprints
|
||||||
Contract.Requires(fingerprinter != null);
|
Contract.Requires(fingerprinter != null);
|
||||||
|
|
||||||
// For attributed file artifacts both path and attributes are critical for fingerprinting
|
// For attributed file artifacts both path and attributes are critical for fingerprinting
|
||||||
fingerprinter.AddNested(fileArtifact.Path, fp => fp.Add("Attributes", (int)fileArtifact.FileExistence));
|
fingerprinter.AddNested(fileArtifact.Path, fp => fp.Add(PipFingerprintField.FileOutput.Attributes, (int)fileArtifact.FileExistence));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -42,7 +42,8 @@ namespace BuildXL.Scheduler.Fingerprints
|
||||||
/// 68: Added ChildProcessesToBreakawayFromSandbox
|
/// 68: Added ChildProcessesToBreakawayFromSandbox
|
||||||
/// 69: Added dynamic existing probe.
|
/// 69: Added dynamic existing probe.
|
||||||
/// 70: Removed duplicates from ObservedAccessedFileNames.
|
/// 70: Removed duplicates from ObservedAccessedFileNames.
|
||||||
|
/// 71: Rename fields in weak fingerprint.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
TwoPhaseV2 = 70,
|
TwoPhaseV2 = 71,
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,6 +5,8 @@ using System;
|
||||||
using System.Diagnostics.ContractsLight;
|
using System.Diagnostics.ContractsLight;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using BuildXL.Engine.Cache.Serialization;
|
using BuildXL.Engine.Cache.Serialization;
|
||||||
|
using BuildXL.Utilities.Configuration;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using static BuildXL.Scheduler.Tracing.FingerprintStoreReader;
|
using static BuildXL.Scheduler.Tracing.FingerprintStoreReader;
|
||||||
|
|
||||||
namespace BuildXL.Scheduler.Tracing
|
namespace BuildXL.Scheduler.Tracing
|
||||||
|
@ -26,6 +28,9 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
/// <nodoc/>
|
/// <nodoc/>
|
||||||
WeakFingerprintMismatch,
|
WeakFingerprintMismatch,
|
||||||
|
|
||||||
|
/// <nodoc/>
|
||||||
|
PathSetHashMismatch,
|
||||||
|
|
||||||
/// <nodoc/>
|
/// <nodoc/>
|
||||||
StrongFingerprintMismatch,
|
StrongFingerprintMismatch,
|
||||||
|
|
||||||
|
@ -57,7 +62,8 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
TextWriter writer,
|
TextWriter writer,
|
||||||
PipCacheMissInfo missInfo,
|
PipCacheMissInfo missInfo,
|
||||||
Func<PipRecordingSession> oldSessionFunc,
|
Func<PipRecordingSession> oldSessionFunc,
|
||||||
Func<PipRecordingSession> newSessionFunc)
|
Func<PipRecordingSession> newSessionFunc,
|
||||||
|
CacheMissDiffFormat diffFormat)
|
||||||
{
|
{
|
||||||
Contract.Requires(oldSessionFunc != null);
|
Contract.Requires(oldSessionFunc != null);
|
||||||
Contract.Requires(newSessionFunc != null);
|
Contract.Requires(newSessionFunc != null);
|
||||||
|
@ -71,7 +77,7 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
case PipCacheMissType.MissForDescriptorsDueToWeakFingerprints:
|
case PipCacheMissType.MissForDescriptorsDueToWeakFingerprints:
|
||||||
case PipCacheMissType.MissForDescriptorsDueToStrongFingerprints:
|
case PipCacheMissType.MissForDescriptorsDueToStrongFingerprints:
|
||||||
// Compute the pip unique output hash to use as the primary lookup key for fingerprint store entries
|
// Compute the pip unique output hash to use as the primary lookup key for fingerprint store entries
|
||||||
return AnalyzeFingerprints(oldSessionFunc, newSessionFunc, writer);
|
return AnalyzeFingerprints(oldSessionFunc, newSessionFunc, writer, diffFormat);
|
||||||
|
|
||||||
// We had a weak and strong fingerprint match, but couldn't retrieve correct data from the cache
|
// We had a weak and strong fingerprint match, but couldn't retrieve correct data from the cache
|
||||||
case PipCacheMissType.MissForCacheEntry:
|
case PipCacheMissType.MissForCacheEntry:
|
||||||
|
@ -106,7 +112,8 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
private static CacheMissAnalysisResult AnalyzeFingerprints(
|
private static CacheMissAnalysisResult AnalyzeFingerprints(
|
||||||
Func<PipRecordingSession> oldSessionFunc,
|
Func<PipRecordingSession> oldSessionFunc,
|
||||||
Func<PipRecordingSession> newSessionFunc,
|
Func<PipRecordingSession> newSessionFunc,
|
||||||
TextWriter writer)
|
TextWriter writer,
|
||||||
|
CacheMissDiffFormat diffFormat)
|
||||||
{
|
{
|
||||||
var result = CacheMissAnalysisResult.Invalid;
|
var result = CacheMissAnalysisResult.Invalid;
|
||||||
|
|
||||||
|
@ -158,19 +165,30 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
if (oldPipSession.FormattedSemiStableHash != newPipSession.FormattedSemiStableHash)
|
if (oldPipSession.FormattedSemiStableHash != newPipSession.FormattedSemiStableHash)
|
||||||
{
|
{
|
||||||
// Make trivial json so the print looks like the rest of the diff
|
// Make trivial json so the print looks like the rest of the diff
|
||||||
var oldNode = new JsonNode
|
if (diffFormat == CacheMissDiffFormat.CustomJsonDiff)
|
||||||
{
|
{
|
||||||
Name = RepeatedStrings.FormattedSemiStableHashChanged
|
var diff = new JProperty("SemiStableHash",
|
||||||
};
|
new JObject(
|
||||||
oldNode.Values.Add(oldPipSession.FormattedSemiStableHash);
|
new JProperty("Old", oldPipSession.FormattedSemiStableHash),
|
||||||
|
new JProperty("New", newPipSession.FormattedSemiStableHash)));
|
||||||
var newNode = new JsonNode
|
WriteLine(new JObject(diff).ToString(), writer);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
Name = RepeatedStrings.FormattedSemiStableHashChanged
|
var oldNode = new JsonNode
|
||||||
};
|
{
|
||||||
newNode.Values.Add(newPipSession.FormattedSemiStableHash);
|
Name = RepeatedStrings.FormattedSemiStableHashChanged
|
||||||
|
};
|
||||||
|
oldNode.Values.Add(oldPipSession.FormattedSemiStableHash);
|
||||||
|
|
||||||
WriteLine(JsonTree.PrintTreeDiff(oldNode, newNode), writer);
|
var newNode = new JsonNode
|
||||||
|
{
|
||||||
|
Name = RepeatedStrings.FormattedSemiStableHashChanged
|
||||||
|
};
|
||||||
|
newNode.Values.Add(newPipSession.FormattedSemiStableHash);
|
||||||
|
|
||||||
|
WriteLine(JsonTree.PrintTreeDiff(oldNode, newNode), writer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diff based off the actual fingerprints instead of the PipCacheMissType
|
// Diff based off the actual fingerprints instead of the PipCacheMissType
|
||||||
|
@ -186,13 +204,47 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
if (oldPipSession.WeakFingerprint != newPipSession.WeakFingerprint)
|
if (oldPipSession.WeakFingerprint != newPipSession.WeakFingerprint)
|
||||||
{
|
{
|
||||||
WriteLine("WeakFingerprint", writer);
|
WriteLine("WeakFingerprint", writer);
|
||||||
WriteLine(JsonTree.PrintTreeDiff(oldPipSession.GetWeakFingerprintTree(), newPipSession.GetWeakFingerprintTree()), writer);
|
|
||||||
|
if (diffFormat == CacheMissDiffFormat.CustomJsonDiff)
|
||||||
|
{
|
||||||
|
WriteLine(oldPipSession.DiffWeakFingerprint(newPipSession).ToString(), writer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteLine(JsonTree.PrintTreeDiff(oldPipSession.GetWeakFingerprintTree(), newPipSession.GetWeakFingerprintTree()), writer);
|
||||||
|
}
|
||||||
|
|
||||||
result = CacheMissAnalysisResult.WeakFingerprintMismatch;
|
result = CacheMissAnalysisResult.WeakFingerprintMismatch;
|
||||||
}
|
}
|
||||||
|
else if (oldPipSession.PathSetHash != newPipSession.PathSetHash)
|
||||||
|
{
|
||||||
|
WriteLine($"PathSet", writer);
|
||||||
|
|
||||||
|
if (diffFormat == CacheMissDiffFormat.CustomJsonDiff)
|
||||||
|
{
|
||||||
|
WriteLine(oldPipSession.DiffPathSet(newPipSession).ToString(), writer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// JsonPatchDiff does not have pathset comparison.
|
||||||
|
WriteLine(JsonTree.PrintTreeDiff(oldPipSession.GetStrongFingerprintTree(), newPipSession.GetStrongFingerprintTree()), writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = CacheMissAnalysisResult.PathSetHashMismatch;
|
||||||
|
}
|
||||||
else if (oldPipSession.StrongFingerprint != newPipSession.StrongFingerprint)
|
else if (oldPipSession.StrongFingerprint != newPipSession.StrongFingerprint)
|
||||||
{
|
{
|
||||||
WriteLine("StrongFingerprint", writer);
|
WriteLine("StrongFingerprint", writer);
|
||||||
WriteLine(JsonTree.PrintTreeDiff(oldPipSession.GetStrongFingerprintTree(), newPipSession.GetStrongFingerprintTree()), writer);
|
|
||||||
|
if (diffFormat == CacheMissDiffFormat.CustomJsonDiff)
|
||||||
|
{
|
||||||
|
WriteLine(oldPipSession.DiffStrongFingerprint(newPipSession).ToString(), writer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteLine(JsonTree.PrintTreeDiff(oldPipSession.GetStrongFingerprintTree(), newPipSession.GetStrongFingerprintTree()), writer);
|
||||||
|
}
|
||||||
|
|
||||||
result = CacheMissAnalysisResult.StrongFingerprintMismatch;
|
result = CacheMissAnalysisResult.StrongFingerprintMismatch;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -237,7 +289,22 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
/// Formatted semi stable hash changed.
|
/// Formatted semi stable hash changed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FormattedSemiStableHashChanged
|
public const string FormattedSemiStableHashChanged
|
||||||
= "FormattedSemiStableHash";
|
= "SemiStableHash";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marker indicating that a value is not specified.
|
||||||
|
/// </summary>
|
||||||
|
public const string UnspecifiedValue = "[Unspecified value]";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marker indicating that a value is specified.
|
||||||
|
/// </summary>
|
||||||
|
public const string ExistentValue = "[Value exists]";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marker indicating that an expected value is missing, which indicates a bug in the cache miss analysis.
|
||||||
|
/// </summary>
|
||||||
|
public const string MissingValue = "Missing value";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,327 @@
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using BuildXL.Scheduler.Fingerprints;
|
||||||
|
using BuildXL.Utilities;
|
||||||
|
|
||||||
|
namespace BuildXL.Scheduler.Tracing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Utilities class for diff-ing fingerprints irrespective of the stored data and of the diff data representation.
|
||||||
|
/// </summary>
|
||||||
|
internal static class FingerprintDiff
|
||||||
|
{
|
||||||
|
#region Extraction
|
||||||
|
private static readonly List<string> s_emptyList = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts differences between two unordered maps (or dictionaries).
|
||||||
|
/// </summary>
|
||||||
|
internal static bool ExtractUnorderedMapDiff<T>(
|
||||||
|
IReadOnlyDictionary<string, T> oldData,
|
||||||
|
IReadOnlyDictionary<string, T> newData,
|
||||||
|
Func<T, T, bool> equalValue,
|
||||||
|
out IReadOnlyList<string> added,
|
||||||
|
out IReadOnlyList<string> removed,
|
||||||
|
out IReadOnlyList<string> changed)
|
||||||
|
{
|
||||||
|
bool hasDiff = ExtractUnorderedListDiff(oldData.Keys, newData.Keys, out added, out removed);
|
||||||
|
List<string> mutableChanged = s_emptyList;
|
||||||
|
|
||||||
|
foreach (var kvp in oldData)
|
||||||
|
{
|
||||||
|
if (newData.TryGetValue(kvp.Key, out var newValue) && !equalValue(kvp.Value, newValue))
|
||||||
|
{
|
||||||
|
if (mutableChanged == s_emptyList)
|
||||||
|
{
|
||||||
|
mutableChanged = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableChanged.Add(kvp.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changed = mutableChanged;
|
||||||
|
|
||||||
|
return hasDiff || changed.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts differences between two unordered lists (or sets).
|
||||||
|
/// </summary>
|
||||||
|
internal static bool ExtractUnorderedListDiff(
|
||||||
|
IEnumerable<string> oldData,
|
||||||
|
IEnumerable<string> newData,
|
||||||
|
out IReadOnlyList<string> added,
|
||||||
|
out IReadOnlyList<string> removed)
|
||||||
|
{
|
||||||
|
bool oldAny = oldData.Any();
|
||||||
|
bool newAny = newData.Any();
|
||||||
|
|
||||||
|
if (!oldAny && !newAny)
|
||||||
|
{
|
||||||
|
added = removed = s_emptyList;
|
||||||
|
}
|
||||||
|
else if (oldAny && !newAny)
|
||||||
|
{
|
||||||
|
added = s_emptyList;
|
||||||
|
removed = oldData.ToHashSet().ToList();
|
||||||
|
}
|
||||||
|
else if (!oldAny && newAny)
|
||||||
|
{
|
||||||
|
removed = s_emptyList;
|
||||||
|
added = newData.ToHashSet().ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var newSet = newData.ToHashSet();
|
||||||
|
var oldSet = oldData.ToHashSet();
|
||||||
|
newSet.ExceptWith(oldData);
|
||||||
|
oldSet.ExceptWith(newData);
|
||||||
|
added = newSet.ToList();
|
||||||
|
removed = oldSet.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return added.Count > 0 || removed.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Extraction
|
||||||
|
|
||||||
|
#region Internal fingerprint data
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Observed input data.
|
||||||
|
/// </summary>
|
||||||
|
internal struct ObservedInputData : IEquatable<ObservedInputData>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Path.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string Path;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flags.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string Flags;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pattern.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string Pattern;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access type.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string AccessType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Content hash or membership hash.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string Hash;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of <see cref="ObservedInputData"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ObservedInputData(
|
||||||
|
string path,
|
||||||
|
string flags,
|
||||||
|
string pattern,
|
||||||
|
string hashMarker,
|
||||||
|
string hash)
|
||||||
|
{
|
||||||
|
Path = path;
|
||||||
|
Flags = flags;
|
||||||
|
Pattern = pattern;
|
||||||
|
AccessType = hashMarker;
|
||||||
|
Hash = hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of <see cref="ObservedInputData"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ObservedInputData(string path, string flags, string pattern) : this(path, flags, pattern, null, null) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool Equals(ObservedInputData other) =>
|
||||||
|
Path == other.Path && Flags == other.Flags && Pattern == other.Pattern && AccessType == other.AccessType && Hash == other.Hash;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool Equals(object obj) => StructUtilities.Equals(this, obj);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCodeHelper.Combine(hashCode(Path), hashCode(Flags), hashCode(Pattern), hashCode(AccessType), hashCode(Hash));
|
||||||
|
|
||||||
|
static int hashCode(string s) => s != null ? EqualityComparer<string>.Default.GetHashCode(s) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes diff with respect to other instance of <see cref="ObservedInputData"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string DescribeDiffWithoutPath(ObservedInputData data) =>
|
||||||
|
string.Join(
|
||||||
|
" | ",
|
||||||
|
(new[] {
|
||||||
|
Prefix(nameof(AccessType), ObservedInputConstants.ToExpandedString(AccessType)),
|
||||||
|
Flags == data.Flags ? null : Prefix(nameof(Flags), Flags),
|
||||||
|
Pattern == data.Pattern ? null : Prefix(nameof(Pattern), Pattern),
|
||||||
|
Hash == data.Hash ? null : Prefix(nameof(Hash), Hash) }).Where(s => !string.IsNullOrEmpty(s)));
|
||||||
|
|
||||||
|
private string Prefix(string prefix, string item) => string.IsNullOrEmpty(item) ? null : prefix + ": " + item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Input file data.
|
||||||
|
/// </summary>
|
||||||
|
internal struct InputFileData : IEquatable<InputFileData>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Path.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string Path;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Content hash or content itself (in case of being written by a write-file pip).
|
||||||
|
/// </summary>
|
||||||
|
public readonly string HashOrContent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of <see cref="InputFileData"/>.
|
||||||
|
/// </summary>
|
||||||
|
public InputFileData(string path, string hashOrContent)
|
||||||
|
{
|
||||||
|
Path = path;
|
||||||
|
HashOrContent = hashOrContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool Equals(InputFileData other) => Path == other.Path && HashOrContent == other.HashOrContent;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool Equals(object obj) => StructUtilities.Equals(this, obj);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCodeHelper.Combine(hashCode(Path), hashCode(HashOrContent));
|
||||||
|
|
||||||
|
static int hashCode(string s) => s != null ? EqualityComparer<string>.Default.GetHashCode(s) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Output file data.
|
||||||
|
/// </summary>
|
||||||
|
internal struct OutputFileData : IEquatable<OutputFileData>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Path.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string Path;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attributes.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of <see cref="OutputFileData"/>.
|
||||||
|
/// </summary>
|
||||||
|
public OutputFileData(string path, string attributes)
|
||||||
|
{
|
||||||
|
Path = path;
|
||||||
|
Attributes = attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool Equals(OutputFileData other) => Path == other.Path && Attributes == other.Attributes;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool Equals(object obj) => StructUtilities.Equals(this, obj);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCodeHelper.Combine(hashCode(Path), hashCode(Attributes));
|
||||||
|
|
||||||
|
static int hashCode(string s) => s != null ? EqualityComparer<string>.Default.GetHashCode(s) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Environment variable data.
|
||||||
|
/// </summary>
|
||||||
|
internal struct EnvironmentVariableData : IEquatable<EnvironmentVariableData>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Name.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string Name;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Value.s
|
||||||
|
/// </summary>
|
||||||
|
public readonly string Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of <see cref="EnvironmentVariableData"/>.
|
||||||
|
/// </summary>
|
||||||
|
public EnvironmentVariableData(string name, string value)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool Equals(EnvironmentVariableData other) => Name == other.Name && Value == other.Value;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool Equals(object obj) => StructUtilities.Equals(this, obj);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCodeHelper.Combine(hashCode(Name), hashCode(Value));
|
||||||
|
|
||||||
|
static int hashCode(string s) => s != null ? EqualityComparer<string>.Default.GetHashCode(s) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Internal fingerprint data
|
||||||
|
|
||||||
|
#region Pools
|
||||||
|
|
||||||
|
private static ObjectPool<Dictionary<string, T>> CreateMapPool<T>() =>
|
||||||
|
new ObjectPool<Dictionary<string, T>>(
|
||||||
|
() => new Dictionary<string, T>(),
|
||||||
|
map => { map.Clear(); return map; });
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pool for <see cref="InputFileData"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static ObjectPool<Dictionary<string, InputFileData>> InputFileDataMapPool { get; } = CreateMapPool<InputFileData>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pool for <see cref="OutputFileData"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static ObjectPool<Dictionary<string, OutputFileData>> OutputFileDataMapPool { get; } = CreateMapPool<OutputFileData>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pool for <see cref="EnvironmentVariableData"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static ObjectPool<Dictionary<string, EnvironmentVariableData>> EnvironmentVariableDataMapPool { get; } = CreateMapPool<EnvironmentVariableData>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pool for <see cref="ObservedInputData"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static ObjectPool<Dictionary<string, ObservedInputData>> ObservedInputDataMapPool { get; } = CreateMapPool<ObservedInputData>();
|
||||||
|
|
||||||
|
#endregion Pools
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ using BuildXL.Engine.Cache.Serialization;
|
||||||
using BuildXL.Pips.Operations;
|
using BuildXL.Pips.Operations;
|
||||||
using BuildXL.Scheduler.Fingerprints;
|
using BuildXL.Scheduler.Fingerprints;
|
||||||
using BuildXL.Utilities;
|
using BuildXL.Utilities;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using static BuildXL.Scheduler.Tracing.FingerprintStore;
|
using static BuildXL.Scheduler.Tracing.FingerprintStore;
|
||||||
|
|
||||||
namespace BuildXL.Scheduler.Tracing
|
namespace BuildXL.Scheduler.Tracing
|
||||||
|
@ -150,6 +151,30 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path set hash of the entry.
|
||||||
|
/// </summary>
|
||||||
|
public string PathSetHash
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
Contract.Assert(EntryExists);
|
||||||
|
return m_entry.StrongFingerprintEntry.PathSetHashToInputs.Key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get path set value of the entry.
|
||||||
|
/// </summary>
|
||||||
|
public string PathSetValue
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
Contract.Assert(EntryExists);
|
||||||
|
return m_entry.StrongFingerprintEntry.PathSetHashToInputs.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor
|
/// Constructor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -172,22 +197,57 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get weak fingerprint tree for the entry
|
/// Get weak fingerprint tree for the entry
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public JsonNode GetWeakFingerprintTree()
|
public JsonNode GetWeakFingerprintTree() => JsonTree.Deserialize(m_entry.WeakFingerprintToInputs.Value);
|
||||||
{
|
|
||||||
return JsonTree.Deserialize(m_entry.WeakFingerprintToInputs.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get strong fingerprint tree for the entry
|
/// Get strong fingerprint tree for the entry
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public JsonNode GetStrongFingerprintTree()
|
public JsonNode GetStrongFingerprintTree() => MergeStrongFingerprintAndPathSetTrees(GetStrongFingerpintInputTree(), GetPathSetTree());
|
||||||
{
|
|
||||||
var strongEntry = m_entry.StrongFingerprintEntry;
|
|
||||||
var strongFingerprintTree = JsonTree.Deserialize(strongEntry.StrongFingerprintToInputs.Value);
|
|
||||||
var pathSetTree = JsonTree.Deserialize(strongEntry.PathSetHashToInputs.Value);
|
|
||||||
|
|
||||||
return MergeStrongFingerprintAndPathSetTrees(strongFingerprintTree, pathSetTree);
|
/// <summary>
|
||||||
}
|
/// Get pathset tree.
|
||||||
|
/// </summary>
|
||||||
|
public JsonNode GetPathSetTree() => JsonTree.Deserialize(m_entry.StrongFingerprintEntry.PathSetHashToInputs.Value);
|
||||||
|
|
||||||
|
private JsonNode GetStrongFingerpintInputTree() => JsonTree.Deserialize(m_entry.StrongFingerprintEntry.StrongFingerprintToInputs.Value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diff pathsets.
|
||||||
|
/// </summary>
|
||||||
|
public JObject DiffPathSet(PipRecordingSession otherSession) =>
|
||||||
|
JsonFingerprintDiff.DiffPathSets(
|
||||||
|
PathSetHash,
|
||||||
|
GetPathSetTree(),
|
||||||
|
GetStrongFingerpintInputTree(),
|
||||||
|
otherSession.PathSetHash,
|
||||||
|
otherSession.GetPathSetTree(),
|
||||||
|
otherSession.GetStrongFingerpintInputTree(),
|
||||||
|
directoryMembershipHash => GetDirectoryMembership(m_store, directoryMembershipHash),
|
||||||
|
otherDirectoryMembershipHash => GetDirectoryMembership(otherSession.m_store, otherDirectoryMembershipHash));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diff strong fingerprints.
|
||||||
|
/// </summary>
|
||||||
|
public JObject DiffStrongFingerprint(PipRecordingSession otherSession) =>
|
||||||
|
JsonFingerprintDiff.DiffStrongFingerprints(
|
||||||
|
StrongFingerprint,
|
||||||
|
GetPathSetTree(),
|
||||||
|
GetStrongFingerpintInputTree(),
|
||||||
|
otherSession.StrongFingerprint,
|
||||||
|
otherSession.GetPathSetTree(),
|
||||||
|
otherSession.GetStrongFingerpintInputTree(),
|
||||||
|
directoryMembershipHash => GetDirectoryMembership(m_store, directoryMembershipHash),
|
||||||
|
otherDirectoryMembershipHash => GetDirectoryMembership(otherSession.m_store, otherDirectoryMembershipHash));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diff weak fingerprints.
|
||||||
|
/// </summary>
|
||||||
|
public JObject DiffWeakFingerprint(PipRecordingSession otherSession) =>
|
||||||
|
JsonFingerprintDiff.DiffWeakFingerprints(
|
||||||
|
WeakFingerprint,
|
||||||
|
GetWeakFingerprintTree(),
|
||||||
|
otherSession.WeakFingerprint,
|
||||||
|
otherSession.GetWeakFingerprintTree());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Path set hash inputs are stored separately from the strong fingerprint inputs.
|
/// Path set hash inputs are stored separately from the strong fingerprint inputs.
|
||||||
|
@ -210,11 +270,11 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
///
|
///
|
||||||
/// From path set hash
|
/// From path set hash
|
||||||
///
|
///
|
||||||
/// [4] "PathSet":""
|
/// [4] "Paths":""
|
||||||
/// [5] "Path":"B:/out/objects/n/x/qbkexxlc8je93wycw7yrlw0a305n7k/xunit-out/CacheMissAnaAD836B23/3/obj/readonly/src_0"
|
/// [5] "Path":"B:/out/objects/n/x/qbkexxlc8je93wycw7yrlw0a305n7k/xunit-out/CacheMissAnaAD836B23/3/obj/readonly/src_0"
|
||||||
/// [6] "Flags":"IsDirectoryPath, DirectoryEnumeration, DirectoryEnumerationWithAllPattern"
|
/// [6] "Flags":"IsDirectoryPath, DirectoryEnumeration, DirectoryEnumerationWithAllPattern"
|
||||||
/// [7] "EnumeratePatternRegex":"^.*$"
|
/// [7] "EnumeratePatternRegex":"^.*$"
|
||||||
///
|
///
|
||||||
/// And end with:
|
/// And end with:
|
||||||
///
|
///
|
||||||
/// [1] "PathSet":"VSO0:7E2E49845EC0AE7413519E3EE605272078AF0B1C2911C021681D1D9197CC134A00"
|
/// [1] "PathSet":"VSO0:7E2E49845EC0AE7413519E3EE605272078AF0B1C2911C021681D1D9197CC134A00"
|
||||||
|
@ -238,10 +298,9 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
|
|
||||||
// In preparation for merging with observed inputs nodes,
|
// In preparation for merging with observed inputs nodes,
|
||||||
// remove the path set node's branch from the path set tree
|
// remove the path set node's branch from the path set tree
|
||||||
// [4] "PathSet":""
|
// [4] "Paths":""
|
||||||
var pathSetNode = JsonTree.FindNodeByName(pathSetTree, ObservedPathEntryConstants.PathSet);
|
var pathSetNode = JsonTree.FindNodeByName(pathSetTree, ObservedPathSet.Labels.Paths);
|
||||||
JsonTree.EmancipateBranch(pathSetNode);
|
JsonTree.EmancipateBranch(pathSetNode);
|
||||||
|
|
||||||
JsonNode currPathNode = null;
|
JsonNode currPathNode = null;
|
||||||
JsonNode currFlagNode = null;
|
JsonNode currFlagNode = null;
|
||||||
JsonNode currRegexNode = null;
|
JsonNode currRegexNode = null;
|
||||||
|
@ -252,23 +311,17 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
switch (child.Name)
|
switch (child.Name)
|
||||||
{
|
{
|
||||||
case ObservedPathEntryConstants.Path:
|
case ObservedPathEntryConstants.Path:
|
||||||
|
if (currPathNode != null)
|
||||||
|
{
|
||||||
|
mergePathSetNode(parentPathNode, currPathNode, currFlagNode, currRegexNode, observedInputIt.Value);
|
||||||
|
observedInputIt = observedInputsNode.Children.First;
|
||||||
|
currPathNode = null;
|
||||||
|
currFlagNode = null;
|
||||||
|
currRegexNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
currPathNode = child;
|
currPathNode = child;
|
||||||
// Switch from literal string "path" to actual file system path
|
JsonTree.EmancipateBranch(currPathNode);
|
||||||
// [5'] "B:/out/objects/n/x/qbkexxlc8je93wycw7yrlw0a305n7k/xunit-out/CacheMissAnaAD836B23/3/obj/readonly/src_0":""
|
|
||||||
currPathNode.Name = currPathNode.Values[0];
|
|
||||||
// The name captures the node's value, so clear the values to avoid extraneous value comparison when diffing
|
|
||||||
currPathNode.Values.Clear();
|
|
||||||
JsonTree.ReparentBranch(currPathNode, parentPathNode);
|
|
||||||
|
|
||||||
// [6'] "Flags":"IsDirectoryPath, DirectoryEnumeration, DirectoryEnumerationWithAllPattern"
|
|
||||||
JsonTree.ReparentBranch(currFlagNode, currPathNode);
|
|
||||||
// [7'] "EnumeratePatternRegex":"^.*$"
|
|
||||||
JsonTree.ReparentBranch(currRegexNode, currPathNode);
|
|
||||||
|
|
||||||
// [3'] "ObservedInput":"E:VSO0:E0C5007DC8CF2D331236F156F136C50CACE2A5D549CD132D9B44ABD1F13D50CC00"
|
|
||||||
// [8] "Members":"[src_1, src_2]"
|
|
||||||
ReparentObservedInput(observedInputIt.Value, currPathNode);
|
|
||||||
observedInputIt = observedInputsNode.Children.First;
|
|
||||||
break;
|
break;
|
||||||
case ObservedPathEntryConstants.Flags:
|
case ObservedPathEntryConstants.Flags:
|
||||||
// [6] "Flags":"IsDirectoryPath, DirectoryEnumeration, DirectoryEnumerationWithAllPattern"
|
// [6] "Flags":"IsDirectoryPath, DirectoryEnumeration, DirectoryEnumerationWithAllPattern"
|
||||||
|
@ -285,6 +338,11 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currPathNode != null)
|
||||||
|
{
|
||||||
|
mergePathSetNode(parentPathNode, currPathNode, currFlagNode, currRegexNode, observedInputIt.Value);
|
||||||
|
}
|
||||||
|
|
||||||
// Re-parent any other branches of the path set tree to the strong fingerprint tree
|
// Re-parent any other branches of the path set tree to the strong fingerprint tree
|
||||||
// so they are still in a full strong fingerprint tree comparison.
|
// so they are still in a full strong fingerprint tree comparison.
|
||||||
// We re-parent under parentPathNode because branches of pathSetTree are elements of PathSet
|
// We re-parent under parentPathNode because branches of pathSetTree are elements of PathSet
|
||||||
|
@ -296,6 +354,26 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
}
|
}
|
||||||
|
|
||||||
return strongFingerprintTree;
|
return strongFingerprintTree;
|
||||||
|
|
||||||
|
void mergePathSetNode(JsonNode parentNode, JsonNode pathNode, JsonNode flagNode, JsonNode regexNode, JsonNode observedInputNode)
|
||||||
|
{
|
||||||
|
// Switch from literal string "path" to actual file system path
|
||||||
|
// [5'] "B:/out/objects/n/x/qbkexxlc8je93wycw7yrlw0a305n7k/xunit-out/CacheMissAnaAD836B23/3/obj/readonly/src_0":""
|
||||||
|
pathNode.Name = pathNode.Values[0];
|
||||||
|
|
||||||
|
// The name captures the node's value, so clear the values to avoid extraneous value comparison when diffing
|
||||||
|
pathNode.Values.Clear();
|
||||||
|
JsonTree.ReparentBranch(pathNode, parentNode);
|
||||||
|
|
||||||
|
// [6'] "Flags":"IsDirectoryPath, DirectoryEnumeration, DirectoryEnumerationWithAllPattern"
|
||||||
|
JsonTree.ReparentBranch(flagNode, pathNode);
|
||||||
|
// [7'] "EnumeratePatternRegex":"^.*$"
|
||||||
|
JsonTree.ReparentBranch(regexNode, pathNode);
|
||||||
|
|
||||||
|
// [3'] "ObservedInput":"E:VSO0:E0C5007DC8CF2D331236F156F136C50CACE2A5D549CD132D9B44ABD1F13D50CC00"
|
||||||
|
// [8] "Members":"[src_1, src_2]"
|
||||||
|
ReparentObservedInput(observedInputNode, pathNode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -390,6 +468,17 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> GetDirectoryMembership(FingerprintStore store, string directoryFingerprint)
|
||||||
|
{
|
||||||
|
if(!store.TryGetContentHashValue(directoryFingerprint, out string storedValue))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var directoryMembershipTree = JsonTree.Deserialize(storedValue);
|
||||||
|
return directoryMembershipTree.Children.First.Value.Values;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes a message to a specific pip's file.
|
/// Writes a message to a specific pip's file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -0,0 +1,683 @@
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.ContractsLight;
|
||||||
|
using System.Linq;
|
||||||
|
using BuildXL.Engine.Cache.Serialization;
|
||||||
|
using BuildXL.Pips.Operations;
|
||||||
|
using BuildXL.Scheduler.Fingerprints;
|
||||||
|
using BuildXL.Utilities;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using static BuildXL.Scheduler.Tracing.FingerprintDiff;
|
||||||
|
|
||||||
|
namespace BuildXL.Scheduler.Tracing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Class for diff-ing weak and strong fingerprints, and present the result as Json object.
|
||||||
|
/// </summary>
|
||||||
|
internal static class JsonFingerprintDiff
|
||||||
|
{
|
||||||
|
#region Diff-ing
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diffs weak fingerprints.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="weakFingerprint">Weak fingerprint.</param>
|
||||||
|
/// <param name="weakFingerprintTree">Weak fingerprint tree.</param>
|
||||||
|
/// <param name="otherWeakFingerprint">Other weak fingerprint.</param>
|
||||||
|
/// <param name="otherWeakFingerprintTree">Other weak fingerprint tree.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static JObject DiffWeakFingerprints(
|
||||||
|
string weakFingerprint,
|
||||||
|
JsonNode weakFingerprintTree,
|
||||||
|
string otherWeakFingerprint,
|
||||||
|
JsonNode otherWeakFingerprintTree)
|
||||||
|
{
|
||||||
|
JObject result = new JObject();
|
||||||
|
|
||||||
|
if (weakFingerprint == otherWeakFingerprint)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// {
|
||||||
|
// WeakFingerprint: { Old: old_weak_fingerprint, New: new_weak_fingerprint }
|
||||||
|
// }
|
||||||
|
AddPropertyIfNotNull(result, RenderSingleValueDiff("WeakFingerprint", weakFingerprint, otherWeakFingerprint));
|
||||||
|
|
||||||
|
using (var weakFingerprintDataPool = JsonNodeMapPool.GetInstance())
|
||||||
|
using (var otherWeakFingerprintDataPool = JsonNodeMapPool.GetInstance())
|
||||||
|
{
|
||||||
|
var weakFingerprintData = weakFingerprintDataPool.Instance;
|
||||||
|
var otherWeakFingerprintData = otherWeakFingerprintDataPool.Instance;
|
||||||
|
|
||||||
|
JsonTree.VisitTree(weakFingerprintTree, wfNode => weakFingerprintData[wfNode.Name] = wfNode, recurse: false);
|
||||||
|
JsonTree.VisitTree(otherWeakFingerprintTree, wfNode => otherWeakFingerprintData[wfNode.Name] = wfNode, recurse: false);
|
||||||
|
|
||||||
|
var fields = new HashSet<string>(weakFingerprintData.Keys.Concat(otherWeakFingerprintData.Keys));
|
||||||
|
|
||||||
|
foreach (var field in fields)
|
||||||
|
{
|
||||||
|
bool getFieldNode = weakFingerprintData.TryGetValue(field, out JsonNode fieldNode);
|
||||||
|
bool getOtherFieldNode = otherWeakFingerprintData.TryGetValue(field, out JsonNode otherFieldNode);
|
||||||
|
|
||||||
|
if (getFieldNode != getOtherFieldNode)
|
||||||
|
{
|
||||||
|
string fieldValue = getFieldNode
|
||||||
|
? (fieldNode.Values != null && fieldNode.Values.Count == 1
|
||||||
|
? fieldNode.Values[0]
|
||||||
|
: CacheMissAnalysisUtilities.RepeatedStrings.ExistentValue)
|
||||||
|
: CacheMissAnalysisUtilities.RepeatedStrings.UnspecifiedValue;
|
||||||
|
string otherFieldValue = getOtherFieldNode
|
||||||
|
? (otherFieldNode.Values != null && otherFieldNode.Values.Count == 1
|
||||||
|
? otherFieldNode.Values[0]
|
||||||
|
: CacheMissAnalysisUtilities.RepeatedStrings.ExistentValue)
|
||||||
|
: CacheMissAnalysisUtilities.RepeatedStrings.UnspecifiedValue;
|
||||||
|
|
||||||
|
AddPropertyIfNotNull(result, RenderSingleValueDiff(field, fieldValue, otherFieldValue));
|
||||||
|
}
|
||||||
|
else if (getFieldNode && getOtherFieldNode)
|
||||||
|
{
|
||||||
|
Contract.Assert(fieldNode != null);
|
||||||
|
Contract.Assert(otherFieldNode != null);
|
||||||
|
|
||||||
|
AddPropertyIfNotNull(result, DiffWeakFingerprintField(fieldNode, otherFieldNode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Contract.Assert(result.Count > 0);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diffs strong fingerprints.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="strongFingerprint">Strong fingerprint.</param>
|
||||||
|
/// <param name="pathSetTree">Pathset tree.</param>
|
||||||
|
/// <param name="strongFingerprintInputTree">Strong fingerprint input tree.</param>
|
||||||
|
/// <param name="otherStrongFingerprint">Other strong fingerprint.</param>
|
||||||
|
/// <param name="otherPathSetTree">Other pathset tree.</param>
|
||||||
|
/// <param name="otherStrongFingerprintInputTree">Other strong fingerprint input tree.</param>
|
||||||
|
/// <param name="getDirectoryMembership">Delegate for getting directory membership.</param>
|
||||||
|
/// <param name="getOtherDirectoryMembership">Delegate for getting other directory membership.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static JObject DiffStrongFingerprints(
|
||||||
|
string strongFingerprint,
|
||||||
|
JsonNode pathSetTree,
|
||||||
|
JsonNode strongFingerprintInputTree,
|
||||||
|
string otherStrongFingerprint,
|
||||||
|
JsonNode otherPathSetTree,
|
||||||
|
JsonNode otherStrongFingerprintInputTree,
|
||||||
|
Func<string, IReadOnlyList<string>> getDirectoryMembership,
|
||||||
|
Func<string, IReadOnlyList<string>> getOtherDirectoryMembership)
|
||||||
|
{
|
||||||
|
JObject result = new JObject();
|
||||||
|
|
||||||
|
if (strongFingerprint == otherStrongFingerprint)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// {
|
||||||
|
// StrongFingerprint: { Old: old_strong_fingerprint, New: new_strong_fingerprint }
|
||||||
|
// }
|
||||||
|
AddPropertyIfNotNull(result, RenderSingleValueDiff("StrongFingerprint", strongFingerprint, otherStrongFingerprint));
|
||||||
|
|
||||||
|
AddPropertyIfNotNull(
|
||||||
|
result,
|
||||||
|
DiffObservedPaths(
|
||||||
|
pathSetTree,
|
||||||
|
strongFingerprintInputTree,
|
||||||
|
otherPathSetTree,
|
||||||
|
otherStrongFingerprintInputTree,
|
||||||
|
getDirectoryMembership,
|
||||||
|
getOtherDirectoryMembership));
|
||||||
|
|
||||||
|
Contract.Assert(result.Count > 0);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diffs strong fingerprints.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pathSetHash">Pathset hash.</param>
|
||||||
|
/// <param name="pathSetTree">Pathset tree.</param>
|
||||||
|
/// <param name="strongFingerprintInputTree">Strong fingerprint input tree.</param>
|
||||||
|
/// <param name="otherPathSetHash">Other pathset hash.</param>
|
||||||
|
/// <param name="otherPathSetTree">Other pathset tree.</param>
|
||||||
|
/// <param name="otherStrongFingerprintInputTree">Other strong fingerprint input tree.</param>
|
||||||
|
/// <param name="getDirectoryMembership">Delegate for getting directory membership.</param>
|
||||||
|
/// <param name="getOtherDirectoryMembership">Delegate for getting other directory membership.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static JObject DiffPathSets(
|
||||||
|
string pathSetHash,
|
||||||
|
JsonNode pathSetTree,
|
||||||
|
JsonNode strongFingerprintInputTree,
|
||||||
|
string otherPathSetHash,
|
||||||
|
JsonNode otherPathSetTree,
|
||||||
|
JsonNode otherStrongFingerprintInputTree,
|
||||||
|
Func<string, IReadOnlyList<string>> getDirectoryMembership,
|
||||||
|
Func<string, IReadOnlyList<string>> getOtherDirectoryMembership)
|
||||||
|
{
|
||||||
|
JObject result = new JObject();
|
||||||
|
|
||||||
|
if (pathSetHash == otherPathSetHash)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// {
|
||||||
|
// PathSetHash: { Old: old_path_set_hash, New: new_path_set_hash }
|
||||||
|
// }
|
||||||
|
AddPropertyIfNotNull(result, RenderSingleValueDiff("PathSetHash", pathSetHash, otherPathSetHash));
|
||||||
|
|
||||||
|
JsonNode unsafeOptionsNode = JsonTree.FindNodeByName(pathSetTree, ObservedPathSet.Labels.UnsafeOptions);
|
||||||
|
JsonNode otherUnsafeOptionsNode = JsonTree.FindNodeByName(otherPathSetTree, ObservedPathSet.Labels.UnsafeOptions);
|
||||||
|
|
||||||
|
// This is less ideal because we can't see the difference.
|
||||||
|
// TODO: dump unsafe option data to the fingerprint store so that we can analyze the content.
|
||||||
|
// {
|
||||||
|
// UnsafeOptions: { Old: old_bits, New: new_bits: }
|
||||||
|
// }
|
||||||
|
AddPropertyIfNotNull(result, RenderSingleValueDiff(ObservedPathSet.Labels.UnsafeOptions, unsafeOptionsNode.Values[0], otherUnsafeOptionsNode.Values[0]));
|
||||||
|
|
||||||
|
AddPropertyIfNotNull(
|
||||||
|
result,
|
||||||
|
DiffObservedPaths(
|
||||||
|
pathSetTree,
|
||||||
|
strongFingerprintInputTree,
|
||||||
|
otherPathSetTree,
|
||||||
|
otherStrongFingerprintInputTree,
|
||||||
|
getDirectoryMembership,
|
||||||
|
getOtherDirectoryMembership));
|
||||||
|
|
||||||
|
JsonNode obsFileNameNode = JsonTree.FindNodeByName(pathSetTree, ObservedPathSet.Labels.ObservedAccessedFileNames);
|
||||||
|
JsonNode otherObsFileNameNode = JsonTree.FindNodeByName(otherPathSetTree, ObservedPathSet.Labels.ObservedAccessedFileNames);
|
||||||
|
|
||||||
|
bool hasDiff = ExtractUnorderedListDiff(obsFileNameNode.Values, otherObsFileNameNode.Values, out var addedFileNames, out var removedFileName);
|
||||||
|
|
||||||
|
if (hasDiff)
|
||||||
|
{
|
||||||
|
result.Add(new JProperty(
|
||||||
|
ObservedPathSet.Labels.ObservedAccessedFileNames,
|
||||||
|
RenderUnorderedListDiff(addedFileNames, removedFileName, RenderPath)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Contract.Assert(result.Count > 0);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JProperty DiffWeakFingerprintField(JsonNode fieldNode, JsonNode otherFieldNode)
|
||||||
|
{
|
||||||
|
Contract.Requires(fieldNode != null);
|
||||||
|
Contract.Requires(otherFieldNode != null);
|
||||||
|
Contract.Requires(fieldNode.Name == otherFieldNode.Name);
|
||||||
|
|
||||||
|
switch (fieldNode.Name)
|
||||||
|
{
|
||||||
|
case nameof(Process.Dependencies):
|
||||||
|
{
|
||||||
|
using (var inputFileDataPool = InputFileDataMapPool.GetInstance())
|
||||||
|
using (var otherInputFileDataPool = InputFileDataMapPool.GetInstance())
|
||||||
|
{
|
||||||
|
var inputFileData = inputFileDataPool.Instance;
|
||||||
|
var otherInputFileData = otherInputFileDataPool.Instance;
|
||||||
|
populateInputFileData(fieldNode, inputFileData);
|
||||||
|
populateInputFileData(otherFieldNode, otherInputFileData);
|
||||||
|
return ExtractUnorderedMapDiff(
|
||||||
|
inputFileData,
|
||||||
|
otherInputFileData,
|
||||||
|
(dOld, dNew) => dOld.Equals(dNew),
|
||||||
|
out var added,
|
||||||
|
out var removed,
|
||||||
|
out var changed)
|
||||||
|
? new JProperty(fieldNode.Name, RenderUnorderedMapDiff(
|
||||||
|
inputFileData,
|
||||||
|
otherInputFileData,
|
||||||
|
added,
|
||||||
|
removed,
|
||||||
|
changed,
|
||||||
|
RenderPath,
|
||||||
|
(dataA, dataB) => dataA.HashOrContent))
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case nameof(Process.FileOutputs):
|
||||||
|
{
|
||||||
|
using (var outputFileDataPool = OutputFileDataMapPool.GetInstance())
|
||||||
|
using (var otherOutputFileDataPool = OutputFileDataMapPool.GetInstance())
|
||||||
|
{
|
||||||
|
var outputFileData = outputFileDataPool.Instance;
|
||||||
|
var otherOutputFileData = otherOutputFileDataPool.Instance;
|
||||||
|
populateOutputFileData(fieldNode, outputFileData);
|
||||||
|
populateOutputFileData(otherFieldNode, otherOutputFileData);
|
||||||
|
return ExtractUnorderedMapDiff(
|
||||||
|
outputFileData,
|
||||||
|
otherOutputFileData,
|
||||||
|
(dOld, dNew) => dOld.Equals(dNew),
|
||||||
|
out var added,
|
||||||
|
out var removed,
|
||||||
|
out var changed)
|
||||||
|
? new JProperty(fieldNode.Name, RenderUnorderedMapDiff(
|
||||||
|
outputFileData,
|
||||||
|
otherOutputFileData,
|
||||||
|
added,
|
||||||
|
removed,
|
||||||
|
changed,
|
||||||
|
RenderPath,
|
||||||
|
(dataA, dataB) => dataA.Attributes))
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case nameof(Process.EnvironmentVariables):
|
||||||
|
{
|
||||||
|
using (var envVarDataPool = EnvironmentVariableDataMapPool.GetInstance())
|
||||||
|
using (var otherEnvVarDataPool = EnvironmentVariableDataMapPool.GetInstance())
|
||||||
|
{
|
||||||
|
var envVarData = envVarDataPool.Instance;
|
||||||
|
var otherEnvVarData = otherEnvVarDataPool.Instance;
|
||||||
|
populateEnvironmentVariableData(fieldNode, envVarData);
|
||||||
|
populateEnvironmentVariableData(otherFieldNode, otherEnvVarData);
|
||||||
|
return ExtractUnorderedMapDiff(
|
||||||
|
envVarData,
|
||||||
|
otherEnvVarData,
|
||||||
|
(dOld, dNew) => dOld.Equals(dNew),
|
||||||
|
out var added,
|
||||||
|
out var removed,
|
||||||
|
out var changed)
|
||||||
|
? new JProperty(fieldNode.Name, RenderUnorderedMapDiff(
|
||||||
|
envVarData,
|
||||||
|
otherEnvVarData,
|
||||||
|
added,
|
||||||
|
removed,
|
||||||
|
changed,
|
||||||
|
k => k,
|
||||||
|
(dataA, dataB) => dataA.Value))
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case nameof(Process.DirectoryDependencies):
|
||||||
|
case nameof(Process.DirectoryOutputs):
|
||||||
|
case nameof(Process.UntrackedPaths):
|
||||||
|
case nameof(Process.UntrackedScopes):
|
||||||
|
case nameof(Process.PreserveOutputWhitelist):
|
||||||
|
case nameof(Process.SuccessExitCodes):
|
||||||
|
case PipFingerprintField.Process.SourceChangeAffectedInputList:
|
||||||
|
case nameof(Process.ChildProcessesToBreakawayFromSandbox):
|
||||||
|
{
|
||||||
|
var data = fieldNode.Values;
|
||||||
|
var otherData = otherFieldNode.Values;
|
||||||
|
return ExtractUnorderedListDiff(data, otherData, out var added, out var removed)
|
||||||
|
? new JProperty(fieldNode.Name, RenderUnorderedListDiff(added, removed, RenderPath))
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return RenderSingleValueDiff(fieldNode.Name, getSingleValueNode(fieldNode), getSingleValueNode(otherFieldNode));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
string getSingleValueNode(JsonNode node) =>
|
||||||
|
node.Values.Count > 0
|
||||||
|
? node.Values[0]
|
||||||
|
: CacheMissAnalysisUtilities.RepeatedStrings.MissingValue;
|
||||||
|
|
||||||
|
void populateInputFileData(JsonNode dependencyNode, Dictionary<string, InputFileData> inputFileData)
|
||||||
|
{
|
||||||
|
JsonTree.VisitTree(
|
||||||
|
dependencyNode,
|
||||||
|
node =>
|
||||||
|
{
|
||||||
|
string value = CacheMissAnalysisUtilities.RepeatedStrings.MissingValue;
|
||||||
|
if (node.Values.Count > 0)
|
||||||
|
{
|
||||||
|
value = node.Values[0];
|
||||||
|
}
|
||||||
|
else if (node.Children.First != null
|
||||||
|
&& node.Children.First.Value.Name == PipFingerprintField.FileDependency.PathNormalizedWriteFileContent
|
||||||
|
&& node.Children.First.Value.Values.Count > 0)
|
||||||
|
{
|
||||||
|
value = node.Children.First.Value.Values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
inputFileData[node.Name] = new InputFileData(node.Name, value);
|
||||||
|
},
|
||||||
|
recurse: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void populateOutputFileData(JsonNode outputNode, Dictionary<string, OutputFileData> outputFileData)
|
||||||
|
{
|
||||||
|
JsonTree.VisitTree(
|
||||||
|
outputNode,
|
||||||
|
node =>
|
||||||
|
{
|
||||||
|
string value = CacheMissAnalysisUtilities.RepeatedStrings.MissingValue;
|
||||||
|
if (node.Children.First != null
|
||||||
|
&& node.Children.First.Value.Name == PipFingerprintField.FileOutput.Attributes
|
||||||
|
&& node.Children.First.Value.Values.Count > 0)
|
||||||
|
{
|
||||||
|
value = node.Children.First.Value.Values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFileData[node.Name] = new OutputFileData(node.Name, value);
|
||||||
|
},
|
||||||
|
recurse: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void populateEnvironmentVariableData(JsonNode environmentVariableNode, Dictionary<string, EnvironmentVariableData> environmentVariableData)
|
||||||
|
{
|
||||||
|
JsonTree.VisitTree(
|
||||||
|
environmentVariableNode,
|
||||||
|
node =>
|
||||||
|
{
|
||||||
|
environmentVariableData[node.Name] = new EnvironmentVariableData(
|
||||||
|
node.Name,
|
||||||
|
node.Values.Count > 0 ? node.Values[0] : CacheMissAnalysisUtilities.RepeatedStrings.MissingValue);
|
||||||
|
},
|
||||||
|
recurse: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JProperty DiffObservedPaths(
|
||||||
|
JsonNode pathSetTree,
|
||||||
|
JsonNode strongFingerprintInputTree,
|
||||||
|
JsonNode otherPathSetTree,
|
||||||
|
JsonNode otherStrongFingerprintInputTree,
|
||||||
|
Func<string, IReadOnlyList<string>> getDirectoryMembership,
|
||||||
|
Func<string, IReadOnlyList<string>> getOtherDirectoryMembership)
|
||||||
|
{
|
||||||
|
JsonNode pathsNode = JsonTree.FindNodeByName(pathSetTree, ObservedPathSet.Labels.Paths);
|
||||||
|
JsonNode otherPathsNode = JsonTree.FindNodeByName(otherPathSetTree, ObservedPathSet.Labels.Paths);
|
||||||
|
|
||||||
|
JsonNode observedInputsTree = JsonTree.FindNodeByName(strongFingerprintInputTree, ObservedInputConstants.ObservedInputs);
|
||||||
|
JsonNode otherObservedInputsTree = JsonTree.FindNodeByName(otherStrongFingerprintInputTree, ObservedInputConstants.ObservedInputs);
|
||||||
|
|
||||||
|
using (var pathSetDataPool = ObservedInputDataMapPool.GetInstance())
|
||||||
|
using (var otherPathSetDataPool = ObservedInputDataMapPool.GetInstance())
|
||||||
|
{
|
||||||
|
var pathSetData = pathSetDataPool.Instance;
|
||||||
|
var otherPathSetData = otherPathSetDataPool.Instance;
|
||||||
|
traversePathSetPaths(pathsNode, observedInputsTree, pathSetData);
|
||||||
|
traversePathSetPaths(otherPathsNode, otherObservedInputsTree, otherPathSetData);
|
||||||
|
|
||||||
|
bool hasDiff = ExtractUnorderedMapDiff(
|
||||||
|
pathSetData,
|
||||||
|
otherPathSetData,
|
||||||
|
(data, otherData) => data.Equals(otherData),
|
||||||
|
out var added,
|
||||||
|
out var removed,
|
||||||
|
out var changed);
|
||||||
|
|
||||||
|
if (hasDiff)
|
||||||
|
{
|
||||||
|
// {
|
||||||
|
// Paths: {
|
||||||
|
// Added : [..paths..],
|
||||||
|
// Removed: [..paths..],
|
||||||
|
// Changed: {
|
||||||
|
// path: { Old: ..., New: ... }
|
||||||
|
// }:
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return new JProperty(
|
||||||
|
ObservedPathSet.Labels.Paths,
|
||||||
|
RenderUnorderedMapDiff(
|
||||||
|
pathSetData,
|
||||||
|
otherPathSetData,
|
||||||
|
added,
|
||||||
|
removed,
|
||||||
|
changed,
|
||||||
|
RenderPath,
|
||||||
|
(dataA, dataB) => dataA.DescribeDiffWithoutPath(dataB),
|
||||||
|
c => diffDirectoryIfApplicable(pathSetData, otherPathSetData, c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JProperty diffDirectoryIfApplicable(Dictionary<string, ObservedInputData> obsInputData, Dictionary<string, ObservedInputData> otherObsInputData, string possiblyChangeDirectory)
|
||||||
|
{
|
||||||
|
// {
|
||||||
|
// Members: {
|
||||||
|
// Added : [..file..],
|
||||||
|
// Removed : [..file..]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
const string MembersLabel = "Members";
|
||||||
|
|
||||||
|
var change = obsInputData[possiblyChangeDirectory];
|
||||||
|
var otherChange = otherObsInputData[possiblyChangeDirectory];
|
||||||
|
if (change.AccessType == ObservedInputConstants.DirectoryEnumeration
|
||||||
|
&& otherChange.AccessType == ObservedInputConstants.DirectoryEnumeration
|
||||||
|
&& change.Pattern == otherChange.Pattern)
|
||||||
|
{
|
||||||
|
var members = getDirectoryMembership(change.Hash);
|
||||||
|
|
||||||
|
if (members == null)
|
||||||
|
{
|
||||||
|
return new JProperty(MembersLabel, $"{CacheMissAnalysisUtilities.RepeatedStrings.MissingDirectoryMembershipFingerprint} ({nameof(ObservedInputData.Hash)}: {change.Hash})");
|
||||||
|
}
|
||||||
|
|
||||||
|
var otherMembers = getOtherDirectoryMembership(otherChange.Hash);
|
||||||
|
|
||||||
|
if (otherMembers == null)
|
||||||
|
{
|
||||||
|
return new JProperty(MembersLabel, $"{CacheMissAnalysisUtilities.RepeatedStrings.MissingDirectoryMembershipFingerprint} ({nameof(ObservedInputData.Hash)}: {otherChange.Hash})");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasDiff = ExtractUnorderedListDiff(members, otherMembers, out var addedMembers, out var removedMembers);
|
||||||
|
|
||||||
|
if (hasDiff)
|
||||||
|
{
|
||||||
|
return new JProperty(MembersLabel, RenderUnorderedListDiff(addedMembers, removedMembers, RenderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void traversePathSetPaths(
|
||||||
|
JsonNode pathSetTree,
|
||||||
|
JsonNode strongFingerprintInputTree,
|
||||||
|
Dictionary<string, ObservedInputData> populatedData)
|
||||||
|
{
|
||||||
|
TraversePathSetPaths(pathSetTree, strongFingerprintInputTree, data => populatedData[data.Path] = data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Diff-ing
|
||||||
|
|
||||||
|
#region Rendering
|
||||||
|
|
||||||
|
private static JObject RenderUnorderedMapDiff<T>(
|
||||||
|
IReadOnlyDictionary<string, T> oldData,
|
||||||
|
IReadOnlyDictionary<string, T> newData,
|
||||||
|
IReadOnlyList<string> added,
|
||||||
|
IReadOnlyList<string> removed,
|
||||||
|
IReadOnlyList<string> changed,
|
||||||
|
Func<string, string> renderKey,
|
||||||
|
Func<T, T, string> describeValueDiff,
|
||||||
|
Func<string, JProperty> extraDiffChange = null)
|
||||||
|
{
|
||||||
|
JObject result = RenderUnorderedListDiff(added, removed, renderKey);
|
||||||
|
|
||||||
|
JProperty changedProperty = null;
|
||||||
|
|
||||||
|
if (changed != null && changed.Count > 0)
|
||||||
|
{
|
||||||
|
changedProperty = new JProperty(
|
||||||
|
"Changed",
|
||||||
|
new JObject(changed.Select(c => RenderSingleValueDiff(
|
||||||
|
renderKey(c),
|
||||||
|
describeValueDiff(oldData[c], newData[c]),
|
||||||
|
describeValueDiff(newData[c], oldData[c]),
|
||||||
|
extraDiffChange)).ToArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null && changedProperty == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
result = new JObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProperty != null)
|
||||||
|
{
|
||||||
|
result.Add(changedProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JObject RenderUnorderedListDiff(
|
||||||
|
IReadOnlyList<string> added,
|
||||||
|
IReadOnlyList<string> removed,
|
||||||
|
Func<string, string> renderItem)
|
||||||
|
{
|
||||||
|
JProperty addedProperty = added != null && added.Count > 0 ? new JProperty("Added", new JArray(added.Select(a => renderItem(a)).ToArray())) : null;
|
||||||
|
JProperty removedProperty = removed != null && removed.Count > 0 ? new JProperty("Removed", new JArray(removed.Select(a => renderItem(a)).ToArray())) : null;
|
||||||
|
|
||||||
|
if (addedProperty == null && removedProperty == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JObject result = new JObject();
|
||||||
|
|
||||||
|
addToResult(addedProperty);
|
||||||
|
addToResult(removedProperty);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
void addToResult(JProperty p)
|
||||||
|
{
|
||||||
|
if (p != null)
|
||||||
|
{
|
||||||
|
result.Add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JProperty RenderSingleValueDiff(string key, string oldValue, string newValue, Func<string, JProperty> extraDiff = null)
|
||||||
|
{
|
||||||
|
if (oldValue == newValue)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var diff = new []
|
||||||
|
{
|
||||||
|
new JProperty("Old", oldValue),
|
||||||
|
new JProperty("New", newValue)
|
||||||
|
};
|
||||||
|
|
||||||
|
var diffObject = new JObject(diff);
|
||||||
|
|
||||||
|
if (extraDiff != null)
|
||||||
|
{
|
||||||
|
JProperty extra = extraDiff(key);
|
||||||
|
if (extra != null)
|
||||||
|
{
|
||||||
|
diffObject.Add(extra);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new JProperty(key, diffObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RenderPath(string path) => path;
|
||||||
|
|
||||||
|
#endregion Rendering
|
||||||
|
|
||||||
|
#region Traversal
|
||||||
|
|
||||||
|
private static void TraversePathSetPaths(
|
||||||
|
JsonNode pathSetPathsNode,
|
||||||
|
JsonNode observedInputs,
|
||||||
|
Action<ObservedInputData> action)
|
||||||
|
{
|
||||||
|
string path = null;
|
||||||
|
string flags = null;
|
||||||
|
string pattern = null;
|
||||||
|
|
||||||
|
string hashMarker = null;
|
||||||
|
string hash = null;
|
||||||
|
|
||||||
|
var obIt = observedInputs?.Children.First;
|
||||||
|
|
||||||
|
for (var it = pathSetPathsNode.Children.First; it != null; it = it.Next)
|
||||||
|
{
|
||||||
|
var elem = it.Value;
|
||||||
|
switch (elem.Name)
|
||||||
|
{
|
||||||
|
case ObservedPathEntryConstants.Path:
|
||||||
|
if (path != null)
|
||||||
|
{
|
||||||
|
action(new ObservedInputData(path, flags, pattern, hashMarker, hash));
|
||||||
|
path = null;
|
||||||
|
flags = null;
|
||||||
|
pattern = null;
|
||||||
|
hashMarker = null;
|
||||||
|
hash = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
path = elem.Values[0];
|
||||||
|
|
||||||
|
if (obIt != null)
|
||||||
|
{
|
||||||
|
hashMarker = obIt.Value.Name;
|
||||||
|
hash = obIt.Value.Values[0];
|
||||||
|
obIt = obIt.Next;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ObservedPathEntryConstants.Flags:
|
||||||
|
Contract.Assert(path != null);
|
||||||
|
flags = elem.Values[0];
|
||||||
|
break;
|
||||||
|
case ObservedPathEntryConstants.EnumeratePatternRegex:
|
||||||
|
Contract.Assert(path != null);
|
||||||
|
pattern = elem.Values[0];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path != null)
|
||||||
|
{
|
||||||
|
action(new ObservedInputData(path, flags, pattern, hashMarker, hash));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Traversal
|
||||||
|
|
||||||
|
#region Utilities
|
||||||
|
|
||||||
|
private static void AddPropertyIfNotNull(JObject o, JProperty p)
|
||||||
|
{
|
||||||
|
if (p != null)
|
||||||
|
{
|
||||||
|
o.Add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ObjectPool<Dictionary<string, JsonNode>> JsonNodeMapPool { get; } =
|
||||||
|
new ObjectPool<Dictionary<string, JsonNode>>(
|
||||||
|
() => new Dictionary<string, JsonNode>(),
|
||||||
|
map => { map.Clear(); return map; });
|
||||||
|
|
||||||
|
#endregion Utilities
|
||||||
|
}
|
||||||
|
}
|
|
@ -278,12 +278,12 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
logStats: false))
|
logStats: false))
|
||||||
{
|
{
|
||||||
UnsafeOptions.Serialize(buildXLWriter);
|
UnsafeOptions.Serialize(buildXLWriter);
|
||||||
writer.Add("SerializedUnsafeOptions", System.BitConverter.ToString(stream.ToArray()));
|
writer.Add(ObservedPathSet.Labels.UnsafeOptions, System.BitConverter.ToString(stream.ToArray()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var thisRef = this;
|
var thisRef = this;
|
||||||
writer.AddNested(ObservedPathEntryConstants.PathSet, w =>
|
writer.AddNested(ObservedPathSet.Labels.Paths, w =>
|
||||||
{
|
{
|
||||||
foreach (var p in thisRef.PathEntries)
|
foreach (var p in thisRef.PathEntries)
|
||||||
{
|
{
|
||||||
|
@ -300,7 +300,7 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
});
|
});
|
||||||
|
|
||||||
writer.AddCollection<StringId, ReadOnlyArray<StringId>>(
|
writer.AddCollection<StringId, ReadOnlyArray<StringId>>(
|
||||||
"ObservedAccessedFileNames",
|
ObservedPathSet.Labels.ObservedAccessedFileNames,
|
||||||
ObservedAccessedFileNames,
|
ObservedAccessedFileNames,
|
||||||
(w, v) => w.Add(v));
|
(w, v) => w.Add(v));
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,14 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
if (possibleStore.Succeeded)
|
if (possibleStore.Succeeded)
|
||||||
{
|
{
|
||||||
Logger.Log.SuccessLoadFingerprintStoreToCompare(loggingContext, option.Mode.ToString(), possibleStore.Result.StoreDirectory);
|
Logger.Log.SuccessLoadFingerprintStoreToCompare(loggingContext, option.Mode.ToString(), possibleStore.Result.StoreDirectory);
|
||||||
return new RuntimeCacheMissAnalyzer(logTarget, loggingContext, context, possibleStore.Result, graph, runnablePipPerformance);
|
return new RuntimeCacheMissAnalyzer(
|
||||||
|
logTarget,
|
||||||
|
loggingContext,
|
||||||
|
context,
|
||||||
|
possibleStore.Result,
|
||||||
|
graph,
|
||||||
|
runnablePipPerformance,
|
||||||
|
configuration.Logging.CacheMissDiffFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Log.GettingFingerprintStoreTrace(loggingContext, I($"Failed to read the fingerprint store to compare. Mode: {option.Mode.ToString()} Failure: {possibleStore.Failure.DescribeIncludingInnerFailures()}"));
|
Logger.Log.GettingFingerprintStoreTrace(loggingContext, I($"Failed to read the fingerprint store to compare. Mode: {option.Mode.ToString()} Failure: {possibleStore.Failure.DescribeIncludingInnerFailures()}"));
|
||||||
|
@ -126,13 +133,16 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public FingerprintStore PreviousFingerprintStore { get; }
|
public FingerprintStore PreviousFingerprintStore { get; }
|
||||||
|
|
||||||
|
private readonly CacheMissDiffFormat m_cacheMissDiffFormat;
|
||||||
|
|
||||||
private RuntimeCacheMissAnalyzer(
|
private RuntimeCacheMissAnalyzer(
|
||||||
FingerprintStoreExecutionLogTarget logTarget,
|
FingerprintStoreExecutionLogTarget logTarget,
|
||||||
LoggingContext loggingContext,
|
LoggingContext loggingContext,
|
||||||
PipExecutionContext context,
|
PipExecutionContext context,
|
||||||
FingerprintStore previousFingerprintStore,
|
FingerprintStore previousFingerprintStore,
|
||||||
IReadonlyDirectedGraph graph,
|
IReadonlyDirectedGraph graph,
|
||||||
IDictionary<PipId, RunnablePipPerformanceInfo> runnablePipPerformance)
|
IDictionary<PipId, RunnablePipPerformanceInfo> runnablePipPerformance,
|
||||||
|
CacheMissDiffFormat cacheMissDiffFormat)
|
||||||
{
|
{
|
||||||
m_loggingContext = loggingContext;
|
m_loggingContext = loggingContext;
|
||||||
m_logTarget = logTarget;
|
m_logTarget = logTarget;
|
||||||
|
@ -142,6 +152,7 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
m_changedPips = new VisitationTracker(graph);
|
m_changedPips = new VisitationTracker(graph);
|
||||||
m_pipCacheMissesDict = new ConcurrentDictionary<PipId, PipCacheMissInfo>();
|
m_pipCacheMissesDict = new ConcurrentDictionary<PipId, PipCacheMissInfo>();
|
||||||
m_runnablePipPerformance = runnablePipPerformance;
|
m_runnablePipPerformance = runnablePipPerformance;
|
||||||
|
m_cacheMissDiffFormat = cacheMissDiffFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AddCacheMiss(PipCacheMissInfo cacheMissInfo)
|
internal void AddCacheMiss(PipCacheMissInfo cacheMissInfo)
|
||||||
|
@ -199,7 +210,8 @@ namespace BuildXL.Scheduler.Tracing
|
||||||
writer,
|
writer,
|
||||||
missInfo,
|
missInfo,
|
||||||
() => new FingerprintStoreReader.PipRecordingSession(PreviousFingerprintStore, oldEntry),
|
() => new FingerprintStoreReader.PipRecordingSession(PreviousFingerprintStore, oldEntry),
|
||||||
() => new FingerprintStoreReader.PipRecordingSession(m_logTarget.ExecutionFingerprintStore, newEntry));
|
() => new FingerprintStoreReader.PipRecordingSession(m_logTarget.ExecutionFingerprintStore, newEntry),
|
||||||
|
m_cacheMissDiffFormat);
|
||||||
|
|
||||||
// The diff sometimes contains several empty new lines at the end.
|
// The diff sometimes contains several empty new lines at the end.
|
||||||
var reason = writer.ToString().TrimEnd(Environment.NewLine.ToCharArray());
|
var reason = writer.ToString().TrimEnd(Environment.NewLine.ToCharArray());
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Test.BuildXL.TestUtilities.Xunit;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
using static BuildXL.Scheduler.Tracing.FingerprintDiff;
|
||||||
|
|
||||||
|
namespace Test.BuildXL.FingerprintStore
|
||||||
|
{
|
||||||
|
public class FingerprintDiffTests : XunitBuildXLTest
|
||||||
|
{
|
||||||
|
public FingerprintDiffTests(ITestOutputHelper output) : base(output)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtractObservedInputsDiff()
|
||||||
|
{
|
||||||
|
var commonData = new[]
|
||||||
|
{
|
||||||
|
new ObservedInputData(A("X", "Y", "f"), "flagF", "*.cs", "F", "XYf123"),
|
||||||
|
new ObservedInputData(A("X", "Y", "g"), "flagG", "*.cc", "G", "XYg123"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var oldData = new Dictionary<string, ObservedInputData>(commonData.ToDictionary(k => k.Path))
|
||||||
|
{
|
||||||
|
[A("X", "Y", "a")] = new ObservedInputData(A("X", "Y", "a"), "flagA", "*.cs", "A", "XYa123"),
|
||||||
|
[A("X", "Y", "b")] = new ObservedInputData(A("X", "Y", "b"), "flagB", "*.cb", "B", "XYb123"),
|
||||||
|
[A("X", "Y", "c")] = new ObservedInputData(A("X", "Y", "c"), "flagC", "*.cc", "C", "XYc123"),
|
||||||
|
[A("X", "Y", "d")] = new ObservedInputData(A("X", "Y", "d"), "flagD", "*.cd", "D", "XYd123"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var newData = new Dictionary<string, ObservedInputData>(commonData.ToDictionary(k => k.Path))
|
||||||
|
{
|
||||||
|
[A("X", "Y", "p")] = new ObservedInputData(A("X", "Y", "p"), "flagP", "*.cp", "P", "XYp123"),
|
||||||
|
[A("X", "Y", "q")] = new ObservedInputData(A("X", "Y", "q"), "flagQ", "*.cq", "Q", "XYq123"),
|
||||||
|
[A("X", "Y", "c")] = new ObservedInputData(A("X", "Y", "c"), "flagC1", "*.cc", "C", "XYc123"),
|
||||||
|
[A("X", "Y", "d")] = new ObservedInputData(A("X", "Y", "d"), "flagD", "*.cd", "D", "XYd124"),
|
||||||
|
};
|
||||||
|
|
||||||
|
bool hasDiff = ExtractUnorderedMapDiff(
|
||||||
|
oldData,
|
||||||
|
newData,
|
||||||
|
(o, n) => o.Equals(n),
|
||||||
|
out var added,
|
||||||
|
out var removed,
|
||||||
|
out var changed);
|
||||||
|
|
||||||
|
XAssert.IsTrue(hasDiff);
|
||||||
|
XAssert.AreEqual(2, added.Count);
|
||||||
|
XAssert.AreEqual(2, removed.Count);
|
||||||
|
XAssert.AreEqual(2, changed.Count);
|
||||||
|
|
||||||
|
XAssert.Contains(added, A("X", "Y", "p"), A("X", "Y", "q"));
|
||||||
|
XAssert.Contains(removed, A("X", "Y", "a"), A("X", "Y", "b"));
|
||||||
|
XAssert.Contains(changed, A("X", "Y", "c"), A("X", "Y", "d"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtractFilesDiff()
|
||||||
|
{
|
||||||
|
var oldData = new[] { A("X", "Y", "a"), A("X", "Y", "b"), A("X", "Y", "c"), A("X", "Y", "d") };
|
||||||
|
var newData = new[] { A("X", "Y", "a"), A("X", "Y", "b"), A("X", "Y", "f"), A("X", "Y", "g") };
|
||||||
|
|
||||||
|
bool hasDiff = ExtractUnorderedListDiff(oldData, newData, out var added, out var removed);
|
||||||
|
|
||||||
|
XAssert.IsTrue(hasDiff);
|
||||||
|
XAssert.AreEqual(2, added.Count);
|
||||||
|
XAssert.AreEqual(2, removed.Count);
|
||||||
|
|
||||||
|
XAssert.Contains(added, A("X", "Y", "f"), A("X", "Y", "g"));
|
||||||
|
XAssert.Contains(removed, A("X", "Y", "c"), A("X", "Y", "d"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Diagnostics.ContractsLight;
|
using System.Diagnostics.ContractsLight;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using BuildXL.Native.Processes;
|
using BuildXL.Native.Processes;
|
||||||
using BuildXL.Utilities;
|
using BuildXL.Utilities;
|
||||||
using BuildXL.Utilities.Collections;
|
using BuildXL.Utilities.Collections;
|
||||||
|
@ -252,12 +253,24 @@ namespace BuildXL.Pips.Operations
|
||||||
[PipCaching(FingerprintingRole = FingerprintingRole.Semantic)]
|
[PipCaching(FingerprintingRole = FingerprintingRole.Semantic)]
|
||||||
public RegexDescriptor WarningRegex { get; }
|
public RegexDescriptor WarningRegex { get; }
|
||||||
|
|
||||||
|
/// <nodoc/>
|
||||||
|
public StringId WarningRegexPattern => WarningRegex.Pattern;
|
||||||
|
|
||||||
|
/// <nodoc/>
|
||||||
|
public RegexOptions WarningRegexOptions => WarningRegex.Options;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional regular expression to detect errors in console error / output.
|
/// Optional regular expression to detect errors in console error / output.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[PipCaching(FingerprintingRole = FingerprintingRole.Semantic)]
|
[PipCaching(FingerprintingRole = FingerprintingRole.Semantic)]
|
||||||
public RegexDescriptor ErrorRegex { get; }
|
public RegexDescriptor ErrorRegex { get; }
|
||||||
|
|
||||||
|
/// <nodoc/>
|
||||||
|
public StringId ErrorRegexPattern => ErrorRegex.Pattern;
|
||||||
|
|
||||||
|
/// <nodoc/>
|
||||||
|
public RegexOptions ErrorRegexOptions => ErrorRegex.Options;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When false (or not set): process output is scanned for error messages line by line;
|
/// When false (or not set): process output is scanned for error messages line by line;
|
||||||
/// 'errorRegex' is applied to each line and if ANY match is found the ENTIRE line is reported.
|
/// 'errorRegex' is applied to each line and if ANY match is found the ENTIRE line is reported.
|
||||||
|
|
|
@ -24,6 +24,8 @@ namespace BuildXL.Execution.Analyzer
|
||||||
bool allPips = false;
|
bool allPips = false;
|
||||||
bool noBanner = false;
|
bool noBanner = false;
|
||||||
long sshValue = -1;
|
long sshValue = -1;
|
||||||
|
CacheMissDiffFormat cacheMissDiffFormat = CacheMissDiffFormat.CustomJsonDiff;
|
||||||
|
|
||||||
foreach (var opt in AnalyzerOptions)
|
foreach (var opt in AnalyzerOptions)
|
||||||
{
|
{
|
||||||
if (opt.Name.Equals("outputDirectory", StringComparison.OrdinalIgnoreCase) ||
|
if (opt.Name.Equals("outputDirectory", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
@ -44,6 +46,10 @@ namespace BuildXL.Execution.Analyzer
|
||||||
{
|
{
|
||||||
noBanner = ParseBooleanOption(opt);
|
noBanner = ParseBooleanOption(opt);
|
||||||
}
|
}
|
||||||
|
else if (opt.Name.Equals("cacheMissDiffFormat", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
cacheMissDiffFormat = ParseEnumOption<CacheMissDiffFormat>(opt);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw Error("Unknown option for cache miss analysis: {0}", opt.Name);
|
throw Error("Unknown option for cache miss analysis: {0}", opt.Name);
|
||||||
|
@ -76,6 +82,7 @@ namespace BuildXL.Execution.Analyzer
|
||||||
writer.WriteOption("outputDirectory", "Required. The directory where to write the results", shortName: "o");
|
writer.WriteOption("outputDirectory", "Required. The directory where to write the results", shortName: "o");
|
||||||
writer.WriteOption("allPips", "Optional. Defaults to false.");
|
writer.WriteOption("allPips", "Optional. Defaults to false.");
|
||||||
writer.WriteOption("pipId", "Optional. Run for specific pip.", shortName: "p");
|
writer.WriteOption("pipId", "Optional. Run for specific pip.", shortName: "p");
|
||||||
|
writer.WriteOption("diffFormat", "Optional. Diff format. Allowed values are JsonDiff and JsonPatchDiff. Defaults to CustomJsonDiff");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,6 +113,11 @@ namespace BuildXL.Execution.Analyzer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long SemiStableHashToRun;
|
public long SemiStableHashToRun;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diff format.
|
||||||
|
/// </summary>
|
||||||
|
public CacheMissDiffFormat CacheMissDiffFormat = CacheMissDiffFormat.CustomJsonDiff;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Analysis model based on the new build.
|
/// Analysis model based on the new build.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -320,7 +332,8 @@ namespace BuildXL.Execution.Analyzer
|
||||||
writer,
|
writer,
|
||||||
miss,
|
miss,
|
||||||
() => m_oldReader.StartPipRecordingSession(pip, pipUniqueOutputHashStr),
|
() => m_oldReader.StartPipRecordingSession(pip, pipUniqueOutputHashStr),
|
||||||
() => m_newCacheLookupReader.StartPipRecordingSession(pip, pipUniqueOutputHashStr));
|
() => m_newCacheLookupReader.StartPipRecordingSession(pip, pipUniqueOutputHashStr),
|
||||||
|
CacheMissDiffFormat);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -328,7 +341,8 @@ namespace BuildXL.Execution.Analyzer
|
||||||
m_writer,
|
m_writer,
|
||||||
miss,
|
miss,
|
||||||
() => m_oldReader.StartPipRecordingSession(pip, pipUniqueOutputHash.ToString()),
|
() => m_oldReader.StartPipRecordingSession(pip, pipUniqueOutputHash.ToString()),
|
||||||
() => m_newReader.StartPipRecordingSession(pip, pipUniqueOutputHash.ToString()));
|
() => m_newReader.StartPipRecordingSession(pip, pipUniqueOutputHash.ToString()),
|
||||||
|
CacheMissDiffFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (analysisResult == CacheMissAnalysisResult.MissingFromOldBuild)
|
if (analysisResult == CacheMissAnalysisResult.MissingFromOldBuild)
|
||||||
|
|
|
@ -138,7 +138,7 @@ namespace Test.Tool.Analyzers
|
||||||
ScheduleRunResult buildB = RunScheduler().AssertCacheMiss(pip.PipId);
|
ScheduleRunResult buildB = RunScheduler().AssertCacheMiss(pip.PipId);
|
||||||
|
|
||||||
messages = new string[] { ArtifactToPrint(dir), ObservedInputType.AbsentPathProbe.ToString(), ObservedInputType.DirectoryEnumeration.ToString() };
|
messages = new string[] { ArtifactToPrint(dir), ObservedInputType.AbsentPathProbe.ToString(), ObservedInputType.DirectoryEnumeration.ToString() };
|
||||||
|
|
||||||
RunAnalyzer(buildA, buildB).AssertPipMiss(pip, PipCacheMissType.MissForDescriptorsDueToStrongFingerprints, messages);
|
RunAnalyzer(buildA, buildB).AssertPipMiss(pip, PipCacheMissType.MissForDescriptorsDueToStrongFingerprints, messages);
|
||||||
|
|
||||||
// Strong fingerprint miss: Added new files to enumerated directory
|
// Strong fingerprint miss: Added new files to enumerated directory
|
||||||
|
@ -147,13 +147,8 @@ namespace Test.Tool.Analyzers
|
||||||
ScheduleRunResult buildC = RunScheduler().AssertCacheMiss(pip.PipId);
|
ScheduleRunResult buildC = RunScheduler().AssertCacheMiss(pip.PipId);
|
||||||
|
|
||||||
messages = new string[] { ArtifactToPrint(dir), Path.GetFileName(ArtifactToPrint(addedFile)), Path.GetFileName(ArtifactToPrint(victimFile)) };
|
messages = new string[] { ArtifactToPrint(dir), Path.GetFileName(ArtifactToPrint(addedFile)), Path.GetFileName(ArtifactToPrint(victimFile)) };
|
||||||
|
|
||||||
RunAnalyzer(buildB, buildC).AssertPipMiss(
|
RunAnalyzer(buildB, buildC).AssertPipMiss(pip, PipCacheMissType.MissForDescriptorsDueToStrongFingerprints, messages);
|
||||||
pip,
|
|
||||||
PipCacheMissType.MissForDescriptorsDueToStrongFingerprints,
|
|
||||||
ArtifactToPrint(dir),
|
|
||||||
Path.GetFileName(ArtifactToPrint(addedFile)),
|
|
||||||
Path.GetFileName(ArtifactToPrint(victimFile)));
|
|
||||||
|
|
||||||
// Strong fingerprint miss: Deleted file in enumerated directory
|
// Strong fingerprint miss: Deleted file in enumerated directory
|
||||||
File.Delete(ArtifactToPrint(victimFile));
|
File.Delete(ArtifactToPrint(victimFile));
|
||||||
|
@ -360,7 +355,7 @@ namespace Test.Tool.Analyzers
|
||||||
// Reset the graph and re-schedule the same pip but with an added command line arg
|
// Reset the graph and re-schedule the same pip but with an added command line arg
|
||||||
ResetPipGraphBuilder();
|
ResetPipGraphBuilder();
|
||||||
|
|
||||||
var mismatchingPipBuilder = this.CreatePipBuilder(new Operation[]
|
var mismatchingPipBuilder = CreatePipBuilder(new Operation[]
|
||||||
{
|
{
|
||||||
outOp
|
outOp
|
||||||
});
|
});
|
||||||
|
@ -420,14 +415,12 @@ namespace Test.Tool.Analyzers
|
||||||
cacheLookupStore.RemoveContentHashForTesting(directoryMembershipFingerprint);
|
cacheLookupStore.RemoveContentHashForTesting(directoryMembershipFingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = RunAnalyzer(buildA, buildB).AssertPipMiss(
|
RunAnalyzer(buildA, buildB).AssertPipMiss(
|
||||||
pip,
|
pip,
|
||||||
PipCacheMissType.MissForDescriptorsDueToStrongFingerprints,
|
PipCacheMissType.MissForDescriptorsDueToStrongFingerprints,
|
||||||
ArtifactToPrint(dir),
|
ArtifactToPrint(dir),
|
||||||
ObservedInputType.AbsentPathProbe.ToString(),
|
ObservedInputType.AbsentPathProbe.ToString(),
|
||||||
ObservedInputType.DirectoryEnumeration.ToString());
|
ObservedInputType.DirectoryEnumeration.ToString());
|
||||||
|
|
||||||
result.AssertAnalyzerOutput(CacheMissAnalysisUtilities.RepeatedStrings.MissingDirectoryMembershipFingerprint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -88,4 +88,28 @@ namespace BuildXL.Utilities.Configuration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CustomPath
|
CustomPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cache miss diff format.
|
||||||
|
/// </summary>
|
||||||
|
public enum CacheMissDiffFormat
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom (i.e., non-standard) Json diff format.
|
||||||
|
/// </summary>
|
||||||
|
CustomJsonDiff,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Json patch diff format.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This format will soon be deprecated because
|
||||||
|
/// - the format is not easy to understand and looks cryptic, and
|
||||||
|
/// - it relies on a buggy thrid-party package.
|
||||||
|
/// However, some customers have already play around with this format. Thus,
|
||||||
|
/// to avoid breaking customers hard, this format is preserved, but needs to be selected
|
||||||
|
/// as the default will be <see cref="CustomJsonDiff"/>.
|
||||||
|
/// </remarks>
|
||||||
|
JsonPatchDiff,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -309,6 +309,11 @@ namespace BuildXL.Utilities.Configuration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CacheMissAnalysisOption CacheMissAnalysisOption { get; }
|
CacheMissAnalysisOption CacheMissAnalysisOption { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diff format for cache miss analysis.
|
||||||
|
/// </summary>
|
||||||
|
CacheMissDiffFormat CacheMissDiffFormat { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether console output should be optimized for Azure DevOps output.
|
/// Whether console output should be optimized for Azure DevOps output.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -43,6 +43,7 @@ namespace BuildXL.Utilities.Configuration.Mutable
|
||||||
FailPipOnFileAccessError = true;
|
FailPipOnFileAccessError = true;
|
||||||
UseCustomPipDescriptionOnConsole = true;
|
UseCustomPipDescriptionOnConsole = true;
|
||||||
CacheMissAnalysisOption = CacheMissAnalysisOption.Disabled();
|
CacheMissAnalysisOption = CacheMissAnalysisOption.Disabled();
|
||||||
|
CacheMissDiffFormat = CacheMissDiffFormat.CustomJsonDiff;
|
||||||
RedirectedLogsDirectory = AbsolutePath.Invalid;
|
RedirectedLogsDirectory = AbsolutePath.Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +125,7 @@ namespace BuildXL.Utilities.Configuration.Mutable
|
||||||
template.CacheMissAnalysisOption.Mode,
|
template.CacheMissAnalysisOption.Mode,
|
||||||
new List<string>(template.CacheMissAnalysisOption.Keys),
|
new List<string>(template.CacheMissAnalysisOption.Keys),
|
||||||
pathRemapper.Remap(template.CacheMissAnalysisOption.CustomPath));
|
pathRemapper.Remap(template.CacheMissAnalysisOption.CustomPath));
|
||||||
|
CacheMissDiffFormat = template.CacheMissDiffFormat;
|
||||||
OptimizeConsoleOutputForAzureDevOps = template.OptimizeConsoleOutputForAzureDevOps;
|
OptimizeConsoleOutputForAzureDevOps = template.OptimizeConsoleOutputForAzureDevOps;
|
||||||
InvocationExpandedCommandLineArguments = template.InvocationExpandedCommandLineArguments;
|
InvocationExpandedCommandLineArguments = template.InvocationExpandedCommandLineArguments;
|
||||||
OptimizeProgressUpdatingForAzureDevOps = template.OptimizeProgressUpdatingForAzureDevOps;
|
OptimizeProgressUpdatingForAzureDevOps = template.OptimizeProgressUpdatingForAzureDevOps;
|
||||||
|
@ -314,6 +316,9 @@ namespace BuildXL.Utilities.Configuration.Mutable
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CacheMissAnalysisOption CacheMissAnalysisOption { get; set; }
|
public CacheMissAnalysisOption CacheMissAnalysisOption { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public CacheMissDiffFormat CacheMissDiffFormat { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool OptimizeConsoleOutputForAzureDevOps { get; set; }
|
public bool OptimizeConsoleOutputForAzureDevOps { get; set; }
|
||||||
|
|
||||||
|
|
Загрузка…
Ссылка в новой задаче