diff --git a/build/NuGetPackages/Microsoft.Build.Framework.nuspec b/build/NuGetPackages/Microsoft.Build.Framework.nuspec index 6a28fd9766a..9dc6184b550 100644 --- a/build/NuGetPackages/Microsoft.Build.Framework.nuspec +++ b/build/NuGetPackages/Microsoft.Build.Framework.nuspec @@ -16,10 +16,12 @@ + + diff --git a/documentation/evaluation-profiling.md b/documentation/evaluation-profiling.md new file mode 100644 index 00000000000..4990e5d37d7 --- /dev/null +++ b/documentation/evaluation-profiling.md @@ -0,0 +1,40 @@ +# MSBuild evaluation profiling + +This branch contains an evaluation profiler, which can help analyze which parts of a project (and any .targets/etc that it imports) are taking the most time to evaluate. + +The profiler is enabled when passing /profileevaluation: as a command-line argument to MSBuild. The provided filename is used for generating a profiler report that looks like the following: + +Pass|File|Line #|Expression|Inc (ms)|Inc (%)|Exc (ms)|Exc (%)|#|Bug +---|---|---:|---|---:|---:|---:|---:|---:|--- +Total Evaluation||||650|100%|17|2.7%|1| +Initial Properties (Pass 0)||||5|0.8%|5|0.8%|1| +Properties (Pass 1)||||360|55.4%|3|0.4%|1| +ItemDefinitionGroup (Pass 2)||||9|1.4%|0|0%|1| +Items (Pass 3)||||63|9.7%|1|0.2%|1| +Lazy Items (Pass 3.1)||||173|26.6%|29|4.5%|1| +UsingTasks (Pass 4)||||8|1.2%|8|1.2%|1| +Targets (Pass 5)||||15|2.3%|1|0.2%|1| +Properties (Pass 1)|MVC.csproj|0|``|351|54%|76|11.7%|1| +Items (Pass 3)|Microsoft.NETCore.App.props|8|`$([Microsoft.Build.Utilities.To...`|32|4.9%|32|4.9%|1| +Lazy Items (Pass 3.1)|Microsoft.NET.Sdk.DefaultItems.props|26|``|95|14.6%|23|3.5%|1| +Lazy Items (Pass 3.1)|Microsoft.NET.Sdk.Web.ProjectSystem.props|29|`$([Microsoft.Build.Utilities.To...`|15|2.3%|15|2.3%|1| +Lazy Items (Pass 3.1)|Microsoft.NET.Sdk.DefaultItems.targets|156|`Condition="'%(LinkBase)' != ''")`|12|1.9%|12|1.9%|1| +Properties (Pass 1)|Microsoft.Common.props|15|`Condition="'$(ImportByWildcardBeforeMicrosoftCommonProps)' == ''")`|12|1.9%|12|1.9%|1| +Properties (Pass 1)|Microsoft.Common.props|63|``|164|25.2%|11|1.7%|1| +Properties (Pass 1)|MVC.csproj.nuget.g.targets|7|``|10|1.6%|10|1.6%|1| +Properties (Pass 1)|Sdk.props|29|``|56|8.6%|10|1.5%|1| +Items (Pass 3)|Microsoft.Common.CurrentVersion.targets|368|`Condition="'$(OutputType)' != 'winmdobj' and '@(_DebugSymbolsIntermediatePath)' == ''")`|9|1.4%|9|1.4%|1| +ItemDefinitionGroup (Pass 2)|Microsoft.Common.CurrentVersion.targets|1661|` - \ No newline at end of file + diff --git a/src/Framework.UnitTests/ProjectEvaluationFinishedEventArgs_Tests.cs b/src/Framework.UnitTests/ProjectEvaluationFinishedEventArgs_Tests.cs new file mode 100644 index 00000000000..911e3eab8d8 --- /dev/null +++ b/src/Framework.UnitTests/ProjectEvaluationFinishedEventArgs_Tests.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for ProjectEvaluationFinishedEventArgs +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Xml; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework.Profiler; +using Microsoft.Build.UnitTests.BackEnd; +using Xunit; + +namespace Microsoft.Build.Framework.UnitTests +{ + public class ProjectEvaluationFinishedEventArgs_Tests + { + /// + /// Roundtrip serialization tests for + /// + [MemberData(nameof(GetProfilerResults))] + [Theory] + public void ProfilerResultRoundTrip(ProfilerResult profilerResult) + { + var writeTranslator = TranslationHelpers.GetWriteTranslator(); + ProfilerResult deserializedResult; + +#if FEATURE_BINARY_SERIALIZATION + writeTranslator.TranslateDotNet(ref profilerResult); +#else + NodePacketTranslator.ProfilerResultTranslator.Translate(writeTranslator, ref profilerResult); +#endif + + var readTranslator = TranslationHelpers.GetReadTranslator(); + +#if FEATURE_BINARY_SERIALIZATION + readTranslator.TranslateDotNet(ref deserializedResult); +#else + NodePacketTranslator.ProfilerResultTranslator.Translate(readTranslator, ref deserializedResult); +#endif + + Assert.Equal(deserializedResult, profilerResult); + } + + private static IEnumerable GetProfilerResults() + { + yield return new object[] { new ProfilerResult(new Dictionary()) }; + + yield return new object[] { new ProfilerResult(new Dictionary + { + {new EvaluationLocation(EvaluationPass.TotalEvaluation, "1", "myFile", 42, "elementName", "elementOrCondition", true), new ProfiledLocation(TimeSpan.MaxValue, TimeSpan.MinValue, 2)}, + {new EvaluationLocation(EvaluationPass.Targets, "1", null, null, null, null, false), new ProfiledLocation(TimeSpan.MaxValue, TimeSpan.MinValue, 2)}, + {new EvaluationLocation(EvaluationPass.LazyItems, "2", null, null, null, null, false), new ProfiledLocation(TimeSpan.Zero, TimeSpan.Zero, 0)} + }) }; + + var element = new ProjectRootElement( + XmlReader.Create(new MemoryStream(Encoding.UTF8.GetBytes( + ""))), + new ProjectRootElementCache(false), false, false); + + yield return new object[] { new ProfilerResult(new Dictionary + { + {new EvaluationLocation(EvaluationPass.UsingTasks, "1", "myFile", 42, "conditionCase"), new ProfiledLocation(TimeSpan.MaxValue, TimeSpan.MinValue, 2)}, + {new EvaluationLocation(EvaluationPass.InitialProperties, "1", "myFile", 42, element), + new ProfiledLocation(TimeSpan.MaxValue, TimeSpan.MinValue, 2)} + }) }; + + } + } +} diff --git a/src/Framework.UnitTests/ProjectFinishedEventArgs_Tests.cs b/src/Framework.UnitTests/ProjectFinishedEventArgs_Tests.cs index b788a4b34d3..ce81c3ba1f3 100644 --- a/src/Framework.UnitTests/ProjectFinishedEventArgs_Tests.cs +++ b/src/Framework.UnitTests/ProjectFinishedEventArgs_Tests.cs @@ -6,7 +6,6 @@ //----------------------------------------------------------------------- using System; - using Microsoft.Build.Framework; using Xunit; @@ -21,7 +20,7 @@ public class ProjectFinishedEventArgs_Tests /// Default event to use in tests. /// private ProjectFinishedEventArgs _baseProjectFinishedEvent = new ProjectFinishedEventArgs("Message", "HelpKeyword", "ProjectFile", true); - + /// /// Trivially exercise event args default ctors to boost Frameworks code coverage /// @@ -50,4 +49,4 @@ public ProjectFinishedEventArgs2() } } } -} \ No newline at end of file +} diff --git a/src/Framework/IProjectElement.cs b/src/Framework/IProjectElement.cs new file mode 100644 index 00000000000..e7f7ef67ad5 --- /dev/null +++ b/src/Framework/IProjectElement.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for exposing a ProjectElement to the appropriate loggers. +//----------------------------------------------------------------------- + +namespace Microsoft.Build.Framework +{ + /// + /// Interface for exposing a ProjectElement to the appropriate loggers + /// + public interface IProjectElement + { + + /// + /// Gets the name of the associated element. + /// Useful for display in some circumstances. + /// + string ElementName { get; } + + + /// + /// The outer markup associated with this project element + /// + string OuterElement { get; } + } +} diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 7bc1de1aa19..35cfe20101c 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -68,6 +68,7 @@ + true @@ -115,6 +116,8 @@ true + + diff --git a/src/Framework/Profiler/EvaluationLocation.cs b/src/Framework/Profiler/EvaluationLocation.cs new file mode 100644 index 00000000000..3ded50d66b4 --- /dev/null +++ b/src/Framework/Profiler/EvaluationLocation.cs @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Location for different elements tracked by the evaluation profiler. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.Framework.Profiler +{ + /// + /// Evaluation main phases used by the profiler + /// + /// + /// Order matters since the profiler pretty printer orders profiled items from top to bottom using + /// the pass they belong to + /// + public enum EvaluationPass : byte + { + /// + TotalEvaluation, + /// + InitialProperties, + /// + Properties, + /// + ItemDefintionGroups, + /// + Items, + /// + LazyItems, + /// + UsingTasks, + /// + Targets + } + + /// + /// Represents a location for different evaluation elements tracked by the EvaluationProfiler. + /// +#if FEATURE_BINARY_SERIALIZATION + [Serializable] +#endif + public struct EvaluationLocation + { + /// + /// Default descriptions for locations that are used in case a description is not provided + /// + private static readonly Dictionary PassDefaultDescription = + new Dictionary + { + {EvaluationPass.TotalEvaluation, "Total Evaluation"}, + {EvaluationPass.InitialProperties, "Initial properties (pass 0)"}, + {EvaluationPass.Properties, "Properties (pass 1)"}, + {EvaluationPass.ItemDefintionGroups, "Item definition groups (pass 2)"}, + {EvaluationPass.Items, "Items (pass 3)"}, + {EvaluationPass.LazyItems, "Lazy items (pass 3.1)"}, + {EvaluationPass.UsingTasks, "Using tasks (pass 4)"}, + {EvaluationPass.Targets, "Targets (pass 5)"}, + }; + + /// + public EvaluationPass EvaluationPass { get; } + + /// + public string EvaluationDescription { get; } + + /// + public string File { get; } + + /// + public int? Line { get; } + + /// + public string ElementName { get; } + + /// + public string ElementOrCondition { get; } + + /// + /// True when is an element + /// + public bool IsElement { get; } + + /// + /// True when is a condition + /// + public bool IsCondition => !IsElement; + + /// + /// Constructs the condition case + /// + public EvaluationLocation(EvaluationPass evaluationPass, string evaluationDescription, string file, int? line, string condition) + : this(evaluationPass, evaluationDescription, file, line, "Condition", condition, isElement: false) + {} + + /// + /// Constructs the project element case + /// + public EvaluationLocation(EvaluationPass evaluationPass, string evaluationDescription, string file, int? line, IProjectElement element) + : this(evaluationPass, evaluationDescription, file, line, element?.ElementName, element?.OuterElement, isElement: true) + {} + + /// + /// Constructs the generic case. + /// + /// + /// Used by serialization/deserialization purposes + /// + public EvaluationLocation(EvaluationPass evaluationPass, string evaluationDescription, string file, int? line, string elementName, string elementOrCondition, bool isElement) + { + EvaluationPass = evaluationPass; + EvaluationDescription = evaluationDescription; + File = file; + Line = line; + ElementName = elementName; + ElementOrCondition = elementOrCondition; + IsElement = isElement; + } + + private static readonly EvaluationLocation Empty = new EvaluationLocation(); + + /// + /// An empty location, used as the starting instance. + /// + public static EvaluationLocation EmptyLocation { get; } = Empty; + + /// + public EvaluationLocation WithEvaluationPass(EvaluationPass evaluationPass, string passDescription = null) + { + return new EvaluationLocation(evaluationPass, passDescription ?? PassDefaultDescription[evaluationPass], + this.File, this.Line, this.ElementName, this.ElementOrCondition, this.IsElement); + } + + /// + public EvaluationLocation WithFile(string file) + { + return new EvaluationLocation(this.EvaluationPass, this.EvaluationDescription, file, null, null, null, this.IsElement); + } + + /// + public EvaluationLocation WithFileLineAndElement(string file, int? line, IProjectElement element) + { + return new EvaluationLocation(this.EvaluationPass, this.EvaluationDescription, file, line, element); + } + + /// + public EvaluationLocation WithFileLineAndCondition(string file, int? line, string condition) + { + return new EvaluationLocation(this.EvaluationPass, this.EvaluationDescription, file, line, condition); + } + + /// + public override bool Equals(object obj) + { + if (obj is EvaluationLocation) + { + var other = (EvaluationLocation) obj; + return + EvaluationPass == other.EvaluationPass && + EvaluationDescription == other.EvaluationDescription && + string.Equals(File, other.File, StringComparison.OrdinalIgnoreCase) && + Line == other.Line && + ElementName == other.ElementName; + } + return false; + } + + /// + public override int GetHashCode() + { + var hashCode = 590978104; + hashCode = hashCode * -1521134295 + base.GetHashCode(); + + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(EvaluationPass); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(EvaluationDescription); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(File?.ToLower()); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Line); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ElementName); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ElementOrCondition); + + return hashCode; + } + + /// + public override string ToString() + { + return $"{EvaluationDescription ?? string.Empty}\t{File ?? string.Empty}\t{Line?.ToString() ?? string.Empty}\t{ElementName ?? string.Empty}"; + } + } +} diff --git a/src/Framework/Profiler/ProfilerResult.cs b/src/Framework/Profiler/ProfilerResult.cs new file mode 100644 index 00000000000..f181b11cf2a --- /dev/null +++ b/src/Framework/Profiler/ProfilerResult.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The result profiling an evaluation. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Microsoft.Build.Framework.Profiler +{ + /// + /// Result of profiling an evaluation + /// +#if FEATURE_BINARY_SERIALIZATION + [Serializable] +#endif + public struct ProfilerResult + { + /// + public IReadOnlyDictionary ProfiledLocations { get; } + + /// + public ProfilerResult(IDictionary profiledLocations) + { + ProfiledLocations = new ReadOnlyDictionary(profiledLocations); + } + + /// + public override bool Equals(object obj) + { + if (!(obj is ProfilerResult)) + { + return false; + } + + var result = (ProfilerResult)obj; + + return (ProfiledLocations == result.ProfiledLocations) || + (ProfiledLocations.Count == result.ProfiledLocations.Count && + !ProfiledLocations.Except(result.ProfiledLocations).Any()); + } + + /// + public override int GetHashCode() + { + return ProfiledLocations.Keys.Aggregate(0, (acum, location) => acum + location.GetHashCode()); + } + } + + /// + /// Result of timing the evaluation of a given element at a given location + /// +#if FEATURE_BINARY_SERIALIZATION + [Serializable] +#endif + public struct ProfiledLocation + { + /// + public TimeSpan InclusiveTime { get; } + + /// + public TimeSpan ExclusiveTime { get; } + + /// + public int NumberOfHits { get; } + + /// + public ProfiledLocation(TimeSpan inclusiveTime, TimeSpan exclusiveTime, int numberOfHits) + { + InclusiveTime = inclusiveTime; + ExclusiveTime = exclusiveTime; + NumberOfHits = numberOfHits; + } + + /// + public override bool Equals(object obj) + { + if (!(obj is ProfiledLocation)) + { + return false; + } + + var location = (ProfiledLocation)obj; + return InclusiveTime.Equals(location.InclusiveTime) && + ExclusiveTime.Equals(location.ExclusiveTime) && + NumberOfHits == location.NumberOfHits; + } + + /// + public override int GetHashCode() + { + var hashCode = -2131368567; + hashCode = hashCode * -1521134295 + base.GetHashCode(); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(InclusiveTime); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ExclusiveTime); + hashCode = hashCode * -1521134295 + NumberOfHits.GetHashCode(); + return hashCode; + } + + /// + public override string ToString() + { + return $"[{InclusiveTime} - {ExclusiveTime}]: {NumberOfHits} hits"; + } + } +} diff --git a/src/Framework/ProjectEvaluationFinishedEventArgs.cs b/src/Framework/ProjectEvaluationFinishedEventArgs.cs index 3e1e49453ca..6c24f2999e1 100644 --- a/src/Framework/ProjectEvaluationFinishedEventArgs.cs +++ b/src/Framework/ProjectEvaluationFinishedEventArgs.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using Microsoft.Build.Framework.Profiler; namespace Microsoft.Build.Framework { @@ -33,5 +34,13 @@ public ProjectEvaluationFinishedEventArgs(string message, params object[] messag /// Gets or sets the full path of the project that started evaluation. /// public string ProjectFile { get; set; } + + /// + /// The result of profiling a project. + /// + /// + /// Null if profiling is not turned on + /// + public ProfilerResult? ProfilerResult { get; set; } } -} \ No newline at end of file +} diff --git a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs index f5cc9a47e4a..a35abe3e055 100644 --- a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs +++ b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs @@ -1125,7 +1125,8 @@ public void InvalidToolsVersionErrors() false, warningsAsErrors: null, warningsAsMessages: null, - enableRestore: false); + enableRestore: false, + profilerLogger: null); } finally { @@ -1329,6 +1330,41 @@ public void ProcessWarnAsMessageSwitchWithCodes() Assert.Equal(expectedWarningsAsMessages, actualWarningsAsMessages, StringComparer.OrdinalIgnoreCase); } + /// + /// Verifies that when the /profileevaluation switch is used with no values that an error is shown. + /// + [Fact] + public void ProcessProfileEvaluationEmpty() + { + CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + + MSBuildApp.GatherCommandLineSwitches(new ArrayList(new[] { "/profileevaluation" }), commandLineSwitches); + + VerifySwitchError(commandLineSwitches, "/profileevaluation", AssemblyResources.GetString("MissingProfileParameterError")); + } + + /// + /// Verifies that when the /profileevaluation switch is used with invalid filenames an error is shown. + /// + [MemberData(nameof(GetInvalidFilenames))] + [Theory] + public void ProcessProfileEvaluationInvalidFilename(string filename) + { + try + { + MSBuildApp.ProcessProfileEvaluationSwitch(new string[] {filename}, new ArrayList()); + Assert.True(false, $"Processing the profile evaluation parameter '{filename}' should have failed"); + } + catch (CommandLineSwitchException) + {} + } + + private static IEnumerable GetInvalidFilenames() + { + yield return new object[] { $"a_file_with${Path.GetInvalidFileNameChars().First()}invalid_chars" }; + yield return new object[] { $"C:\\a_path\\with{Path.GetInvalidPathChars().First()}invalid\\chars" }; + } + #if FEATURE_RESOURCEMANAGER_GETRESOURCESET /// /// Verifies that help messages are correctly formed with the right width and leading spaces. diff --git a/src/MSBuild/CommandLineSwitches.cs b/src/MSBuild/CommandLineSwitches.cs index 8ade965357c..4895c56fd00 100644 --- a/src/MSBuild/CommandLineSwitches.cs +++ b/src/MSBuild/CommandLineSwitches.cs @@ -104,7 +104,8 @@ internal enum ParameterizedSwitch WarningsAsMessages, BinaryLogger, Restore, - NumberOfParameterizedSwitches + ProfileEvaluation, + NumberOfParameterizedSwitches, } /// @@ -283,6 +284,7 @@ bool emptyParametersAllowed new ParameterizedSwitchInfo( new string[] { "warnasmessage", "nowarn" }, ParameterizedSwitch.WarningsAsMessages, null, true, "MissingWarnAsMessageParameterError", true, false ), new ParameterizedSwitchInfo( new string[] { "binarylogger", "bl" }, ParameterizedSwitch.BinaryLogger, null, false, null, true, false ), new ParameterizedSwitchInfo( new string[] { "restore", "r" }, ParameterizedSwitch.Restore, null, false, null, true, false ), + new ParameterizedSwitchInfo( new string[] { "profileevaluation", "prof" }, ParameterizedSwitch.ProfileEvaluation, null, false, "MissingProfileParameterError", true, false ), }; /// diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index 62543e0342a..9fec71a9fe3 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -1081,11 +1081,22 @@ Copyright (C) Microsoft Corporation. All rights reserved. LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. - + + /profileevaluation:<file> + Profiles MSBuild evaluation and writes the result + to the specified file. + + + + MSBUILD : error MSB1053: Provided filename is not valid. {0} + + + MSBUILD :error MSB1054: A filename must be specified to generate the profiler result. + diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 3fef3a9f5cb..1c56e940b65 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -21,6 +21,7 @@ using Microsoft.Build.Exceptions; using Microsoft.Build.Execution; using Microsoft.Build.Framework; +using Microsoft.Build.Logging; using Microsoft.Build.Shared; using Microsoft.Build.Utilities; @@ -553,6 +554,7 @@ string [] commandLine ISet warningsAsErrors = null; ISet warningsAsMessages = null; bool enableRestore = Traits.Instance.EnableRestoreFirst; + ProfilerLogger profilerLogger = null; CommandLineSwitches switchesFromAutoResponseFile; CommandLineSwitches switchesNotFromAutoResponseFile; @@ -580,6 +582,7 @@ string [] commandLine ref warningsAsErrors, ref warningsAsMessages, ref enableRestore, + ref profilerLogger, recursing: false )) { @@ -616,7 +619,7 @@ string [] commandLine #if FEATURE_XML_SCHEMA_VALIDATION needToValidateProject, schemaFile, #endif - cpuCount, enableNodeReuse, preprocessWriter, debugger, detailedSummary, warningsAsErrors, warningsAsMessages, enableRestore)) + cpuCount, enableNodeReuse, preprocessWriter, debugger, detailedSummary, warningsAsErrors, warningsAsMessages, enableRestore, profilerLogger)) { exitType = ExitType.BuildError; } @@ -766,6 +769,7 @@ string [] commandLine return exitType; } + #if (!STANDALONEBUILD) /// /// Use the Orcas Engine to build the project @@ -912,7 +916,8 @@ internal static bool BuildProject bool detailedSummary, ISet warningsAsErrors, ISet warningsAsMessages, - bool enableRestore + bool enableRestore, + ProfilerLogger profilerLogger ) { if (String.Equals(Path.GetExtension(projectFile), ".vcproj", StringComparison.OrdinalIgnoreCase) || @@ -1079,6 +1084,13 @@ bool enableRestore parameters.WarningsAsErrors = warningsAsErrors; parameters.WarningsAsMessages = warningsAsMessages; + // Propagate the profiler flag into the project load settings so the evaluator + // can pick it up + if (profilerLogger != null) + { + parameters.ProjectLoadSettings |= ProjectLoadSettings.ProfileEvaluation; + } + if (!String.IsNullOrEmpty(toolsVersion)) { parameters.DefaultToolsVersion = toolsVersion; @@ -1883,6 +1895,7 @@ private static bool ProcessCommandLineSwitches ref ISet warningsAsErrors, ref ISet warningsAsMessages, ref bool enableRestore, + ref ProfilerLogger profilerLogger, bool recursing ) { @@ -1996,6 +2009,7 @@ bool recursing ref warningsAsErrors, ref warningsAsMessages, ref enableRestore, + ref profilerLogger, recursing: true ); } @@ -2050,11 +2064,13 @@ bool recursing commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.FileLoggerParameters], // used by DistributedFileLogger commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.ConsoleLoggerParameters], commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.BinaryLogger], + commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.ProfileEvaluation], groupedFileLoggerParameters, out distributedLoggerRecords, out verbosity, ref detailedSummary, - cpuCount + cpuCount, + out profilerLogger ); // If we picked up switches from the autoreponse file, let the user know. This could be a useful @@ -2230,6 +2246,51 @@ internal static bool ProcessRestoreSwitch(string[] parameters) return enableRestore; } + /// + /// Processes the profiler evaluation switch + /// + /// + /// If the switch is provided, it adds a to the collection of loggers + /// and also returns the created logger. Otherwise, the collection of loggers is not affected and null + /// is returned + /// + internal static ProfilerLogger ProcessProfileEvaluationSwitch(string[] parameters, ArrayList loggers) + { + if (parameters == null || parameters.Length == 0) + { + return null; + } + + var profilerFile = parameters[parameters.Length - 1]; + + // Check if the file name is valid + try + { + new FileInfo(profilerFile); + } + catch (ArgumentException ex) + { + CommandLineSwitchException.Throw("InvalidProfilerValue", parameters[parameters.Length - 1], + ex.Message); + } + catch (PathTooLongException ex) + { + CommandLineSwitchException.Throw("InvalidProfilerValue", parameters[parameters.Length - 1], + ex.Message); + } + catch (NotSupportedException ex) + { + CommandLineSwitchException.Throw("InvalidProfilerValue", parameters[parameters.Length - 1], + ex.Message); + } + + + var logger = new ProfilerLogger(profilerFile); + loggers.Add(logger); + + return logger; + } + /// /// Uses the input from thinNodeMode switch to start a local node server /// @@ -2704,11 +2765,13 @@ private static ILogger[] ProcessLoggingSwitches string[] fileLoggerParameters, string[] consoleLoggerParameters, string[] binaryLoggerParameters, + string[] profileEvaluationParameters, string[][] groupedFileLoggerParameters, out List distributedLoggerRecords, out LoggerVerbosity verbosity, ref bool detailedSummary, - int cpuCount + int cpuCount, + out ProfilerLogger profilerLogger ) { // if verbosity level is not specified, use the default @@ -2733,6 +2796,8 @@ int cpuCount ProcessBinaryLogger(binaryLoggerParameters, loggers, ref verbosity); + profilerLogger = ProcessProfileEvaluationSwitch(profileEvaluationParameters, loggers); + if (verbosity == LoggerVerbosity.Diagnostic) { detailedSummary = true; @@ -3441,6 +3506,7 @@ private static void ShowHelpMessage() } #endif Console.WriteLine(AssemblyResources.GetString("HelpMessage_31_RestoreSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_32_ProfilerSwitch")); Console.WriteLine(AssemblyResources.GetString("HelpMessage_7_ResponseFile")); Console.WriteLine(AssemblyResources.GetString("HelpMessage_8_NoAutoResponseSwitch")); Console.WriteLine(AssemblyResources.GetString("HelpMessage_5_NoLogoSwitch")); diff --git a/src/Shared/INodePacketTranslator.cs b/src/Shared/INodePacketTranslator.cs index 29c52574564..3bbbc24fc48 100644 --- a/src/Shared/INodePacketTranslator.cs +++ b/src/Shared/INodePacketTranslator.cs @@ -123,12 +123,24 @@ BinaryWriter Writer /// The value to be translated. void Translate(ref int value); + /// + /// Translates a long. + /// + /// The value to be translated. + void Translate(ref long value); + /// /// Translates a string. /// /// The value to be translated. void Translate(ref string value); + /// + /// Translates a double. + /// + /// The value to be translated. + void Translate(ref double value); + /// /// Translates a string array. /// @@ -171,6 +183,12 @@ BinaryWriter Writer /// The value to be translated. void Translate(ref DateTime value); + /// + /// Translates a TimeSpan. + /// + /// The value to be translated. + void Translate(ref TimeSpan value); + // MSBuildTaskHost is based on CLR 3.5, which does not have the 6-parameter constructor for BuildEventContext, // which is what current implementations of this method use. However, it also does not ever need to translate // BuildEventContexts, so it should be perfectly safe to compile this method out of that assembly. I am compiling diff --git a/src/Shared/NodePacketTranslator.cs b/src/Shared/NodePacketTranslator.cs index 66c2f0e639f..96186abc194 100644 --- a/src/Shared/NodePacketTranslator.cs +++ b/src/Shared/NodePacketTranslator.cs @@ -8,6 +8,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Text; @@ -15,6 +16,8 @@ using System.Threading; #if FEATURE_BINARY_SERIALIZATION using System.Runtime.Serialization.Formatters.Binary; +#else +using Microsoft.Build.Framework.Profiler; #endif using Microsoft.Build.Collections; using Microsoft.Build.Execution; @@ -152,6 +155,24 @@ public void Translate(ref int value) value = _reader.ReadInt32(); } + /// + /// Translates a long. + /// + /// The value to be translated. + public void Translate(ref long value) + { + value = _reader.ReadInt64(); + } + + /// + /// Translates a double. + /// + /// The value to be translated. + public void Translate(ref double value) + { + value = _reader.ReadDouble(); + } + /// /// Translates a string. /// @@ -293,6 +314,17 @@ public void Translate(ref DateTime value) value = new DateTime(_reader.ReadInt64(), kind); } + /// + /// Translates a TimeSpan. + /// + /// The value to be translated. + public void Translate(ref TimeSpan value) + { + long ticks = 0; + Translate(ref ticks); + value = new System.TimeSpan(ticks); + } + // MSBuildTaskHost is based on CLR 3.5, which does not have the 6-parameter constructor for BuildEventContext. // However, it also does not ever need to translate BuildEventContexts, so it should be perfectly safe to // compile this method out of that assembly. @@ -761,6 +793,24 @@ public void Translate(ref int value) _writer.Write(value); } + /// + /// Translates a long. + /// + /// The value to be translated. + public void Translate(ref long value) + { + _writer.Write(value); + } + + /// + /// Translates a double. + /// + /// The value to be translated. + public void Translate(ref double value) + { + _writer.Write(value); + } + /// /// Translates a string. /// @@ -892,6 +942,15 @@ public void Translate(ref DateTime value) _writer.Write(value.Ticks); } + /// + /// Translates a TimeSpan. + /// + /// The value to be translated. + public void Translate(ref TimeSpan value) + { + _writer.Write(value.Ticks); + } + // MSBuildTaskHost is based on CLR 3.5, which does not have the 6-parameter constructor for BuildEventContext. // However, it also does not ever need to translate BuildEventContexts, so it should be perfectly safe to // compile this method out of that assembly. @@ -1360,6 +1419,17 @@ public void Translate(INodePacketTranslator translator) SerializedBuildEventArgs.TranslateBuildEventContext(translator, ref val); FieldValue = val; } + else if (fieldType == typeof(ProfilerResult?)) + { + var val = (ProfilerResult?)FieldValue; + if (translator.TranslateNullable(val)) + { + var profilerResult = val ?? default(ProfilerResult); + ProfilerResultTranslator.Translate(translator, ref profilerResult); + val = profilerResult; + } + FieldValue = val; + } else if (fieldType == typeof(IDictionary)) { Dictionary val = null; @@ -1414,6 +1484,100 @@ public static void DeserializeFields(object obj, List serialize } } + public static class ProfilerResultTranslator + { + public static void Translate(INodePacketTranslator translator, ref ProfilerResult profilerResult) + { + IDictionary profiledLocations = new Dictionary(); + if (translator.Mode == TranslationDirection.WriteToStream) + { + profiledLocations = profilerResult.ProfiledLocations.ToDictionary(kv => kv.Key, kv => kv.Value); + } + + translator.TranslateDictionary(ref profiledLocations, + TranslatorForEvaluationLocation, TranslatorForProfiledLocation, + count => new Dictionary()); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + profilerResult = new ProfilerResult(new ReadOnlyDictionary(profiledLocations)); + } + } + + private static void TranslatorForEvaluationLocation(ref EvaluationLocation evaluationLocation, + INodePacketTranslator translator) + { + EvaluationPass evaluationPass = default(EvaluationPass); + string evaluationPassDescription = null; + string file = null; + int? line = null; + string elementName = null; + string elementOrCondition = null; + bool isElement = false; + + if (translator.Mode == TranslationDirection.WriteToStream) + { + evaluationPass = evaluationLocation.EvaluationPass; + evaluationPassDescription = evaluationLocation.EvaluationDescription; + file = evaluationLocation.File; + line = evaluationLocation.Line; + elementName = evaluationLocation.ElementName; + elementOrCondition = evaluationLocation.ElementOrCondition; + isElement = evaluationLocation.IsElement; + } + + translator.TranslateEnum(ref evaluationPass, (int)evaluationPass); + translator.Translate(ref evaluationPassDescription); + translator.Translate(ref file); + + if (translator.TranslateNullable(line)) + { + var lineValue = 0; + if (translator.Mode == TranslationDirection.WriteToStream) + { + lineValue = line.Value; + } + translator.Translate(ref lineValue); + if (translator.Mode == TranslationDirection.ReadFromStream) + { + line = lineValue; + } + } + + translator.Translate(ref elementName); + translator.Translate(ref elementOrCondition); + translator.Translate(ref isElement); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + evaluationLocation = new EvaluationLocation(evaluationPass, evaluationPassDescription, file, line, elementName, elementOrCondition, isElement); + } + } + + private static void TranslatorForProfiledLocation(ref ProfiledLocation profiledLocation, INodePacketTranslator translator) + { + var inclusiveTime = TimeSpan.Zero; + var exclusiveTime = TimeSpan.Zero; + var numberOfHits = 0; + + if (translator.Mode == TranslationDirection.WriteToStream) + { + inclusiveTime = profiledLocation.InclusiveTime; + exclusiveTime = profiledLocation.ExclusiveTime; + numberOfHits = profiledLocation.NumberOfHits; + } + + translator.Translate(ref inclusiveTime); + translator.Translate(ref exclusiveTime); + translator.Translate(ref numberOfHits); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + profiledLocation = new ProfiledLocation(inclusiveTime, exclusiveTime, numberOfHits); + } + } + } + private class SerializedBuildEventArgs : INodePacketTranslatable { public AssemblyLoadInfo EventArgsAssembly;