src/StructuredLogger/Analyzers/CppAnalyzer.cs (338 lines of code) (raw):

using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; namespace Microsoft.Build.Logging.StructuredLogger { public class CppAnalyzer { public class CppTask : Task { public bool HasTimedBlocks { get; set; } } public class CppTimedNode { public DateTime StartTime; public DateTime EndTime; public string Text; public int NodeId; public BaseNode Node; } public class CppAnalyzerNode : TimedNode { CppAnalyzer cppAnalyzer; public CppAnalyzerNode(CppAnalyzer cppAnalyzer) { this.IsVisible = false; this.cppAnalyzer = cppAnalyzer; } public CppAnalyzer GetCppAnalyzer() => cppAnalyzer; } // parse for: // midl.exe will run on 8 out of 8 file(s) in 8 batches. Startup phase took 46.0028ms. // Task 'CodeStore.idl' took 2244ms. // Cleanup phase took 68ms. const string regexStartupPhase = @"^.*\.exe will run on (?'activeFiles'[0-9]*) out of (?'totalFiles'[0-9]*) file\(s\) in [0-9]* batches\.\s*Startup phase took (?'msTime'[0-9]*.[0-9]*)ms.$"; readonly Regex startupPhase = new Regex(regexStartupPhase, RegexOptions.Multiline); const string regexCleanupPhase = @"^Cleanup phase took (?'msTime'[0-9]*.[0-9]*)ms.$"; readonly Regex cleanupPhase = new Regex(regexCleanupPhase, RegexOptions.Multiline); const string regexTaskTime = @"^Task '(?'filename'.*)' took (?'msTime'([0-9]*\.[0-9]+|[0-9]+))ms.$"; readonly Regex TaskTime = new Regex(regexTaskTime, RegexOptions.Multiline); // time(C:\Program Files (x86)\Microsoft Visual Studio\2019\Preview\VC\Tools\MSVC\14.24.28218\bin\Hostx86\x86\c1xx.dll)=0.83512s < 985096605139 - 985104956295 > BB [C:\Users\yuehuang\AppData\Local\Temp\123\main36.cpp] // time(C:\Program Files(x86)\Microsoft Visual Studio\2019\Preview\VC\Tools\MSVC\14.24.28218\bin\Hostx86\x86\c2.dll)=0.01935s < 985104875296 - 985105068765 > BB[C: \Users\yuehuang\AppData\Local\Temp\123\main47.cpp] const string regexBTPlus = @"^time\(.*(c1xx\.dll|c2\.dll)\)=(?'msTime'([0-9]*\.[0-9]+|[0-9]+))s \< (?'startTime'[\d]*) - (?'endTime'[\d]*) \>\s*(BB)?\s*\[(?'filename'[^\]]*)\]$"; readonly Regex BTPlus = new Regex(regexBTPlus, RegexOptions.Multiline); // Lib: Final Total time = 0.00804s < 5881693617253 - 5881693697673 > PB: 143409152 [D:\test\ConsoleApplication2\x64\Debug\ConsoleApplication2.lib] // note: there is trailing white space from the tool const string regexLibFinalTime = @"^Lib: Final Total time = (?'msTime'([0-9]*\.[0-9]+|[0-9]+))s \< (?'startTime'[\d]*) - (?'endTime'[\d]*) \>\s*PB:\s*[\d]* \[(?'filename'[^\]]*)\]\s*$"; readonly Regex libFinalTime = new Regex(regexLibFinalTime, RegexOptions.Multiline); // Pass 1: Interval #1, time = 0.125s // Wait PDB close: Total time = 0.000s // Wait type merge: Total time = 0.000s // Pass 2: Interval #2, time = 0.016s // Final: Total time = 0.141s const string regexLinkTotalTime = @"^Final: Total time = (?'msTime'[0-9]*.[0-9]*)s$"; readonly Regex linkTotalTime = new Regex(regexLinkTotalTime, RegexOptions.Multiline); private const string MultiToolTaskName = "MultiToolTask"; private const string CLTaskName = "CL"; private const string LinkTaskName = "Link"; private const string LibTaskName = "LIB"; private const string filenameRegexMatchName = "filename"; private const string startTimeRegexMatchName = "startTime"; private const string endTimeRegexMatchName = "endTime"; private const string msTimeRegexMatchName = "msTime"; private const string btplusKeyword = @"time("; private const string mttKeyword = " took "; private const string mttCleanUpKeyword = "Cleanup phase took "; private const string mttStartUpKeyword = "will run on "; private const string libKeyword = "Lib: Final Total time ="; private const string linkKeyword = "Final: Total time ="; private bool globalBtplus = false; private bool globalLibTime = false; private bool globalLinkTime = false; private TimeSpan oneMilliSecond = TimeSpan.FromMilliseconds(1); private static HashSet<string> hashCppTasks = new HashSet<string>() { MultiToolTaskName, CLTaskName, LinkTaskName, LibTaskName }; private List<CppTimedNode> resultTimedNode = new List<CppTimedNode>(); public CppAnalyzer() { globalBtplus = false; globalLibTime = false; globalLinkTime = false; } public void AnalyzeEnvironment(NamedNode node) { // Search for /Bt+ and /TIME in the environment variables node.VisitAllChildren<Property>(envProperty => { if (envProperty.Name == "_CL_" || envProperty.Name == "__CL__") { globalBtplus = envProperty.Value.Contains("/Bt+"); } else if (envProperty.Name == "_LINK_" || envProperty.Name == "__LINK__") { globalLinkTime = envProperty.Value.Contains("/TIME"); } else if (envProperty.Name == "_LIB_" || envProperty.Name == "__LIB__") { globalLibTime = envProperty.Value.Contains("/TIME"); } }); } public void AppendCppAnalyzer(Build build) { build.AddChild(new CppAnalyzerNode(this)); } public IEnumerable<CppTimedNode> GetAnalyzedTimedNode() { return resultTimedNode; } public void AnalyzeTask(CppTask cppTask) { // MultiToolTask batches tasks and runs them in parallel. // For this view, de-batch them into individual task units. if ((cppTask.Name == MultiToolTaskName || cppTask.Name == CLTaskName) && cppTask.HasChildren) { bool usingBTTime = globalBtplus; Dictionary<string, CppTimedNode> blocks = new(); DateTime taskStartTime = cppTask.StartTime; DateTime taskCleanUpTime = cppTask.EndTime; foreach (var child in cppTask.Children) { if (child is TimedMessage message) { DateTime endTime = DateTime.MinValue; DateTime startTime = DateTime.MinValue; string messageText = message.Text; string filename; if ((usingBTTime && message.Text.StartsWith(btplusKeyword)) || (!usingBTTime && message.Text.Contains(mttKeyword))) { Match match = usingBTTime ? BTPlus.Match(message.Text) : TaskTime.Match(message.Text); if (match.Success) { if (usingBTTime) { // Matching Bt+ filename = match.Groups[filenameRegexMatchName].Value; string startTimeValue = match.Groups[startTimeRegexMatchName].Value; string endTimeValue = match.Groups[endTimeRegexMatchName].Value; string isC1 = match.Groups[endTimeRegexMatchName].Value; if (long.TryParse(startTimeValue, out long tryStartTime) && long.TryParse(endTimeValue, out long tryEndTime) && !string.IsNullOrWhiteSpace(filename)) { startTime = new DateTime(tryStartTime); endTime = new DateTime(tryEndTime); messageText = Path.GetFileName(filename); } } else { // MTT messages only print duration, assume that timestamp of the message is the end. // Round 10ms from end time to compensate for the latency in logging and to make the graph look better. filename = match.Groups[filenameRegexMatchName].Value; string msTime = match.Groups[msTimeRegexMatchName].Value; if (double.TryParse(msTime, out double tryValue) && !string.IsNullOrWhiteSpace(filename)) { startTime = message.Timestamp - TimeSpan.FromMilliseconds(tryValue); endTime = message.Timestamp - TimeSpan.FromMilliseconds(10); messageText = Path.GetFileName(filename); } } if (startTime > DateTime.MinValue) { if (blocks.TryGetValue(filename, out CppTimedNode cppNode)) { // note: there is a strange bug where some messages are printed twice. Don't insert twice into the dictionary. // BT Time prints C1 and C2 as seperate messages, so lets combine to reduce the graph nodes by half. if (usingBTTime) { cppNode.StartTime = new DateTime(Math.Min(cppNode.StartTime.Ticks, startTime.Ticks)); cppNode.EndTime = new DateTime(Math.Max(cppNode.EndTime.Ticks, endTime.Ticks)); } } else { var block = new CppTimedNode() { StartTime = startTime, EndTime = endTime, Text = messageText, Node = message, NodeId = cppTask.NodeId, }; blocks.Add(filename, block); } } } } else if (cppTask.Name == MultiToolTaskName && message.Text.Contains(mttCleanUpKeyword) || message.Text.Contains(mttStartUpKeyword)) { var match = startupPhase.Match(message.Text); if (match.Success) { string msTime = match.Groups[msTimeRegexMatchName].Value; if (double.TryParse(msTime, out double tryValue)) { startTime = message.Timestamp - TimeSpan.FromMilliseconds(tryValue) + oneMilliSecond; endTime = message.Timestamp; taskStartTime = message.Timestamp; } } else { match = cleanupPhase.Match(message.Text); string msTime = match.Groups[msTimeRegexMatchName].Value; if (double.TryParse(msTime, out double tryValue)) { startTime = message.Timestamp - TimeSpan.FromMilliseconds(tryValue) + oneMilliSecond; endTime = message.Timestamp; // taskCleanUpTime is the start time of the cleanup step. taskCleanUpTime = message.Timestamp - TimeSpan.FromMilliseconds(tryValue); } } if (startTime > DateTime.MinValue) { var block = new CppTimedNode() { StartTime = startTime, EndTime = endTime, Text = messageText, Node = message, NodeId = cppTask.NodeId, }; // Add these messages to directly to the lane as to avoid mixing with the Bt+ messages. resultTimedNode.Add(block); cppTask.HasTimedBlocks = true; } } } if (!usingBTTime && child is Property property) { if (property.Name == Strings.CommandLineArguments && property.Value.Contains("/Bt+")) { usingBTTime = true; } } } if (usingBTTime && blocks.Count > 0) { // BT+ timestamp is not a global time, but is relative to the first instance (see QueryPerformanceCounter) // so compute the offset and align it to the right. DateTime offset = blocks.Values.Min(p => p.StartTime); TimeSpan totalDuration = blocks.Values.Max(p => p.EndTime) - offset; var innerDuration = taskCleanUpTime - taskStartTime; if (totalDuration > TimeSpan.Zero) { taskStartTime += TimeSpan.FromTicks((innerDuration - totalDuration).Ticks); foreach (CppTimedNode block in blocks.Values) { block.StartTime = taskStartTime + block.StartTime.Subtract(offset); block.EndTime = taskStartTime + block.EndTime.Subtract(offset); } } } if (blocks.Count > 0) { resultTimedNode.AddRange(blocks.Values); cppTask.HasTimedBlocks = true; } } else if (cppTask.Name == LibTaskName && cppTask.HasChildren) { bool usingLibTime = globalLibTime; foreach (var child in cppTask.Children) { if (usingLibTime && child is TimedMessage message && message.Text.Contains(libKeyword)) { DateTime endTime = DateTime.MinValue; DateTime startTime = DateTime.MinValue; string messageText = message.Text; var match = libFinalTime.Match(message.Text); if (match.Success) { string filename = match.Groups[filenameRegexMatchName].Value; string startTimeValue = match.Groups[startTimeRegexMatchName].Value; string endTimeValue = match.Groups[endTimeRegexMatchName].Value; if (long.TryParse(startTimeValue, out long tryStartTime) && long.TryParse(endTimeValue, out long tryEndTime) && !string.IsNullOrWhiteSpace(filename)) { var duration = tryEndTime - tryStartTime; startTime = message.Timestamp - TimeSpan.FromTicks(duration); endTime = message.Timestamp; messageText = Path.GetFileName(filename); } } if (startTime > DateTime.MinValue) { var block = new CppTimedNode() { StartTime = startTime, EndTime = endTime, Text = messageText, Node = message, NodeId = cppTask.NodeId, }; resultTimedNode.Add(block); cppTask.HasTimedBlocks = true; } } else if (!usingLibTime && child is Property property) { if (property.Name == Strings.CommandLineArguments && property.Value.Contains("/TIME")) { usingLibTime = true; } } } } else if (cppTask.Name == LinkTaskName && cppTask.HasChildren) { bool usingLinkTime = globalLinkTime; foreach (var child in cppTask.Children) { if (child is TimedMessage message && message.Text.Contains(linkKeyword)) { DateTime endTime = DateTime.MinValue; DateTime startTime = DateTime.MinValue; string messageText = message.Text; var match = linkTotalTime.Match(message.Text); if (match.Success) { string secTime = match.Groups[msTimeRegexMatchName].Value; if (double.TryParse(secTime, out double trySecTime)) { startTime = message.Timestamp - TimeSpan.FromSeconds(trySecTime); endTime = message.Timestamp; } } if (startTime > DateTime.MinValue) { var block = new CppTimedNode() { StartTime = startTime, EndTime = endTime, Text = messageText, Node = message, NodeId = cppTask.NodeId, }; resultTimedNode.Add(block); cppTask.HasTimedBlocks = true; } } else if (!usingLinkTime && child is Property property) { if (property.Name == Strings.CommandLineArguments && property.Value.Contains("/TIME")) { usingLinkTime = true; } } } } } } }