// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using EpicGames.Core; using Microsoft.Extensions.Logging; namespace UnrealBuildTool { /// /// Public interface for a timeline scope. Should be disposed to exit the scope. /// interface ITimelineEvent : IDisposable { void Finish(); } /// /// Tracks simple high-level timing data /// static class Timeline { /// /// A marker in the timeline /// [DebuggerDisplay("{Name}")] class Event : ITimelineEvent { /// /// Name of the marker /// public string Name; /// /// Time at which the event ocurred /// public TimeSpan StartTime; /// /// Time at which the event ended /// public TimeSpan? FinishTime; /// /// The trace span for external tracing /// public ITraceSpan Span; /// /// Constructor /// /// Event name /// Time of the event /// Finish time for the event. May be null. public Event(string Name, TimeSpan StartTime, TimeSpan? FinishTime) { this.Name = Name; this.StartTime = StartTime; this.FinishTime = FinishTime; this.Span = TraceSpan.Create(Name); } /// /// Finishes the current event /// public void Finish() { if(!FinishTime.HasValue) { FinishTime = Stopwatch.Elapsed; } Span.Dispose(); } /// /// Disposes of the current event /// public void Dispose() { Finish(); } } /// /// The stopwatch used for timing /// static Stopwatch Stopwatch = new Stopwatch(); /// /// The recorded events /// static List Events = new List(); /// /// Property for the total time elapsed /// public static TimeSpan Elapsed { get { return Stopwatch.Elapsed; } } /// /// Start the stopwatch /// public static void Start() { Stopwatch.Restart(); } /// /// Records a timeline marker with the given name /// /// The marker name public static void AddEvent(string Name) { TimeSpan Time = Stopwatch.Elapsed; Events.Add(new Event(Name, Time, Time)); } /// /// Enters a scope event with the given name. Should be disposed to terminate the scope. /// /// Name of the event /// Event to track the length of the event public static ITimelineEvent ScopeEvent(string Name) { Event Event = new Event(Name, Stopwatch.Elapsed, null); Events.Add(Event); return Event; } /// /// Prints this information to the log /// public static void Print(TimeSpan MaxUnknownTime, LogLevel Verbosity, ILogger Logger) { // Print the start time Logger.Log(Verbosity, "Timeline:"); Logger.Log(Verbosity, ""); Logger.Log(Verbosity, "[{Time,6}]", FormatTime(TimeSpan.Zero)); // Create the root event TimeSpan FinishTime = Stopwatch.Elapsed; List OuterEvents = new List(); OuterEvents.Add(new Event("", TimeSpan.Zero, FinishTime)); // Print out all the child events TimeSpan LastTime = TimeSpan.Zero; for(int EventIdx = 0; EventIdx < Events.Count; EventIdx++) { Event Event = Events[EventIdx]; // Pop events off the stack for (; OuterEvents.Count > 1; OuterEvents.RemoveAt(OuterEvents.Count - 1)) { Event OuterEvent = OuterEvents.Last(); if (Event.StartTime < OuterEvent.FinishTime!.Value) { break; } UpdateLastEventTime(ref LastTime, OuterEvent.FinishTime.Value, MaxUnknownTime, OuterEvents, Verbosity, Logger); } // If there's a gap since the last event, print an unknown marker UpdateLastEventTime(ref LastTime, Event.StartTime, MaxUnknownTime, OuterEvents, Verbosity, Logger); // Print this event Print(Event.StartTime, Event.FinishTime, Event.Name, OuterEvents, Verbosity, Logger); // Push it onto the stack if(Event.FinishTime.HasValue) { if(EventIdx + 1 < Events.Count && Events[EventIdx + 1].StartTime < Event.FinishTime.Value) { OuterEvents.Add(Event); } else { LastTime = Event.FinishTime.Value; } } } // Remove everything from the stack for(; OuterEvents.Count > 0; OuterEvents.RemoveAt(OuterEvents.Count - 1)) { UpdateLastEventTime(ref LastTime, OuterEvents.Last().FinishTime!.Value, MaxUnknownTime, OuterEvents, Verbosity, Logger); } // Print the finish time Logger.Log(Verbosity, "[{Time,6}]", FormatTime(FinishTime)); } /// /// Updates the last event time /// /// /// /// /// /// /// static void UpdateLastEventTime(ref TimeSpan LastTime, TimeSpan NewTime, TimeSpan MaxUnknownTime, List OuterEvents, LogLevel Verbosity, ILogger Logger) { const string UnknownEvent = ""; if (NewTime - LastTime > MaxUnknownTime) { Print(LastTime, NewTime, UnknownEvent, OuterEvents, Verbosity, Logger); } LastTime = NewTime; } /// /// Prints an individual event to the log /// /// Start time for the event /// Finish time for the event. May be null. /// Event name /// List of all the start times for parent events /// Verbosity for the output /// Logger for output static void Print(TimeSpan StartTime, TimeSpan? FinishTime, string Label, List OuterEvents, LogLevel Verbosity, ILogger Logger) { StringBuilder Prefix = new StringBuilder(); for(int Idx = 0; Idx < OuterEvents.Count - 1; Idx++) { Prefix.AppendFormat(" {0,6} ", FormatTime(StartTime - OuterEvents[Idx].StartTime)); } Prefix.AppendFormat("[{0,6}]", FormatTime(StartTime - OuterEvents[OuterEvents.Count - 1].StartTime)); if (!FinishTime.HasValue) { Prefix.AppendFormat("({0,6})", "???"); } else if(FinishTime.Value == StartTime) { Prefix.Append(" ------ "); } else { Prefix.AppendFormat("({0,6})", "+" + FormatTime(FinishTime.Value - StartTime)); } Logger.Log(Verbosity, "{Prefix} {Label}", Prefix.ToString(), Label); } /// /// Formats a timespan in milliseconds /// /// The time to format /// Formatted timespan static string FormatTime(TimeSpan Time) { int TotalMilliseconds = (int)Time.TotalMilliseconds; return String.Format("{0}.{1:000}", TotalMilliseconds / 1000, TotalMilliseconds % 1000); } } }