// 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);
}
}
}