// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; 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 readonly string Name; /// /// Time at which the event ocurred /// public readonly TimeSpan StartTime; /// /// Time at which the event ended /// public TimeSpan? FinishTime; /// /// The trace span for external tracing /// public readonly ITraceSpan Span; /// /// Optional output logger /// ILogger? Logger; /// /// Optional output verbosity /// LogLevel? Verbosity; /// /// Constructor /// /// Event name /// Time of the event /// Finish time for the event. May be null. /// Logger. May be null. /// Verbosity. May be null. public Event(string Name, TimeSpan StartTime, TimeSpan? FinishTime = null, ILogger? Logger = null, LogLevel? Verbosity = null) { this.Name = Name; this.StartTime = StartTime; this.FinishTime = FinishTime; this.Logger = Logger; this.Verbosity = Verbosity; Span = TraceSpan.Create(Name); } /// /// Finishes the current event /// public void Finish() { if (!FinishTime.HasValue) { FinishTime = Stopwatch.Elapsed; if (Logger != null && Verbosity != null) { double Duration = ((TimeSpan)FinishTime - StartTime).TotalSeconds; Logger.Log((LogLevel)Verbosity, "{Name} took {Duration:0.00}s", Name, Duration); } } 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 => Stopwatch.Elapsed; /// /// Start the stopwatch /// public static void Start() { Stopwatch.Restart(); } /// /// Stop the stopwatch /// public static void Stop() { Stopwatch.Stop(); } /// /// Records a timeline marker with the given name /// /// The marker name public static void AddEvent(string Name) { TimeSpan Time = Stopwatch.Elapsed; lock (Events) { 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); lock (Events) { Events.Add(Event); } return Event; } /// /// Enters a scope event with the given name. Should be disposed to terminate the scope. /// Also logs the finish of the event at the provided verbosity. /// /// Name of the event /// Logger. /// LogLevel of the event. /// Event to track the length of the event public static ITimelineEvent ScopeEvent(string Name, ILogger Logger, LogLevel Verbosity = LogLevel.Information) { Event Event = new Event(Name, Stopwatch.Elapsed, null, Logger, Verbosity); lock (Events) { Events.Add(Event); } return Event; } /// /// Prints this information to the log /// /// Non-instrumented time limit for inserting an "unknown" event /// Instrumented time limit for printing the labelled event at given verbosity. May be null. /// LogLevel of the event. /// Logger. public static void Print(TimeSpan? MinKnownTime, TimeSpan MaxUnknownTime, LogLevel Verbosity, ILogger Logger) { // Print the start time Logger.Log(Verbosity, ""); Logger.Log(Verbosity, "Timeline:"); // Create the root event TimeSpan FinishTime = Stopwatch.Elapsed; List OuterEvents = new List { new Event("", TimeSpan.Zero, FinishTime) }; // Print out all the child events TimeSpan LastTime = TimeSpan.Zero; lock (Events) { 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, MinKnownTime, 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,7}]", 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, null, 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. /// MinKnownTime for printing at the provided verbosity. 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, TimeSpan? MinKnownTime, string Label, List OuterEvents, LogLevel Verbosity, ILogger Logger) { StringBuilder Prefix = new StringBuilder(); for (int Idx = 0; Idx < OuterEvents.Count - 1; Idx++) { Prefix.AppendFormat(" {0,7} ", FormatTime(StartTime - OuterEvents[Idx].StartTime)); } Prefix.AppendFormat("[{0,7}]", FormatTime(StartTime - OuterEvents[^1].StartTime)); bool UseDebugVerbosity = MinKnownTime != null; if (!FinishTime.HasValue) { Prefix.AppendFormat("{0,8}", "???"); } else if (FinishTime.Value == StartTime) { Prefix.Append(" ------ "); } else { Prefix.AppendFormat("{0,8}", "+" + FormatTime(FinishTime.Value - StartTime)); UseDebugVerbosity &= (FinishTime.Value - StartTime) < MinKnownTime; } LogLevel PrintVerbosity = UseDebugVerbosity && Verbosity > LogLevel.Debug ? LogLevel.Debug : Verbosity; Logger.Log(PrintVerbosity, "{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); } } }