using System; using System.Diagnostics; using System.Collections.Generic; using System.Text; using System.Linq; namespace Ink.Runtime { /// <summary> /// Simple ink profiler that logs every instruction in the story and counts frequency and timing. /// To use: /// /// var profiler = story.StartProfiling(), /// /// (play your story for a bit) /// /// var reportStr = profiler.Report(); /// /// story.EndProfiling(); /// /// </summary> public class Profiler { /// <summary> /// The root node in the hierarchical tree of recorded ink timings. /// </summary> public ProfileNode rootNode { get { return _rootNode; } } public Profiler() { _rootNode = new ProfileNode(); } /// <summary> /// Generate a printable report based on the data recording during profiling. /// </summary> public string Report() { var sb = new StringBuilder(); sb.AppendFormat("{0} CONTINUES / LINES:\n", _numContinues); sb.AppendFormat("TOTAL TIME: {0}\n", FormatMillisecs(_continueTotal)); sb.AppendFormat("SNAPSHOTTING: {0}\n", FormatMillisecs(_snapTotal)); sb.AppendFormat("OTHER: {0}\n", FormatMillisecs(_continueTotal - (_stepTotal + _snapTotal))); sb.Append(_rootNode.ToString()); return sb.ToString(); } public void PreContinue() { _continueWatch.Reset(); _continueWatch.Start(); } public void PostContinue() { _continueWatch.Stop(); _continueTotal += Millisecs(_continueWatch); _numContinues++; } public void PreStep() { _currStepStack = null; _stepWatch.Reset(); _stepWatch.Start(); } public void Step(CallStack callstack) { _stepWatch.Stop(); var stack = new string[callstack.elements.Count]; for(int i=0; i<stack.Length; i++) { string stackElementName = ""; if(!callstack.elements[i].currentPointer.isNull) { var objPath = callstack.elements[i].currentPointer.path; for(int c=0; c<objPath.length; c++) { var comp = objPath.GetComponent(c); if( !comp.isIndex ) { stackElementName = comp.name; break; } } } stack[i] = stackElementName; } _currStepStack = stack; var currObj = callstack.currentElement.currentPointer.Resolve(); string stepType = null; var controlCommandStep = currObj as ControlCommand; if( controlCommandStep ) stepType = controlCommandStep.commandType.ToString() + " CC"; else stepType = currObj.GetType().Name; _currStepDetails = new StepDetails { type = stepType, obj = currObj }; _stepWatch.Start(); } public void PostStep() { _stepWatch.Stop(); var duration = Millisecs(_stepWatch); _stepTotal += duration; _rootNode.AddSample(_currStepStack, duration); _currStepDetails.time = duration; _stepDetails.Add(_currStepDetails); } /// <summary> /// Generate a printable report specifying the average and maximum times spent /// stepping over different internal ink instruction types. /// This report type is primarily used to profile the ink engine itself rather /// than your own specific ink. /// </summary> public string StepLengthReport() { var sb = new StringBuilder(); sb.AppendLine("TOTAL: "+_rootNode.totalMillisecs+"ms"); var averageStepTimes = _stepDetails .GroupBy(s => s.type) .Select(typeToDetails => new KeyValuePair<string, double>(typeToDetails.Key, typeToDetails.Average(d => d.time))) .OrderByDescending(stepTypeToAverage => stepTypeToAverage.Value) .Select(stepTypeToAverage => { var typeName = stepTypeToAverage.Key; var time = stepTypeToAverage.Value; return typeName + ": " + time + "ms"; }) .ToArray(); sb.AppendLine("AVERAGE STEP TIMES: "+string.Join(", ", averageStepTimes)); var accumStepTimes = _stepDetails .GroupBy(s => s.type) .Select(typeToDetails => new KeyValuePair<string, double>(typeToDetails.Key + " (x"+typeToDetails.Count()+")", typeToDetails.Sum(d => d.time))) .OrderByDescending(stepTypeToAccum => stepTypeToAccum.Value) .Select(stepTypeToAccum => { var typeName = stepTypeToAccum.Key; var time = stepTypeToAccum.Value; return typeName + ": " + time; }) .ToArray(); sb.AppendLine("ACCUMULATED STEP TIMES: "+string.Join(", ", accumStepTimes)); return sb.ToString(); } /// <summary> /// Create a large log of all the internal instructions that were evaluated while profiling was active. /// Log is in a tab-separated format, for easy loading into a spreadsheet application. /// </summary> public string Megalog() { var sb = new StringBuilder(); sb.AppendLine("Step type\tDescription\tPath\tTime"); foreach(var step in _stepDetails) { sb.Append(step.type); sb.Append("\t"); sb.Append(step.obj.ToString()); sb.Append("\t"); sb.Append(step.obj.path); sb.Append("\t"); sb.AppendLine(step.time.ToString("F8")); } return sb.ToString(); } public void PreSnapshot() { _snapWatch.Reset(); _snapWatch.Start(); } public void PostSnapshot() { _snapWatch.Stop(); _snapTotal += Millisecs(_snapWatch); } double Millisecs(Stopwatch watch) { var ticks = watch.ElapsedTicks; return ticks * _millisecsPerTick; } public static string FormatMillisecs(double num) { if( num > 5000 ) { return string.Format("{0:N1} secs", num / 1000.0); } if( num > 1000 ) { return string.Format("{0:N2} secs", num / 1000.0); } else if( num > 100 ) { return string.Format("{0:N0} ms", num); } else if( num > 1 ) { return string.Format("{0:N1} ms", num); } else if( num > 0.01 ) { return string.Format("{0:N3} ms", num); } else { return string.Format("{0:N} ms", num); } } Stopwatch _continueWatch = new Stopwatch(); Stopwatch _stepWatch = new Stopwatch(); Stopwatch _snapWatch = new Stopwatch(); double _continueTotal; double _snapTotal; double _stepTotal; string[] _currStepStack; StepDetails _currStepDetails; ProfileNode _rootNode; int _numContinues; struct StepDetails { public string type; public Runtime.Object obj; public double time; } List<StepDetails> _stepDetails = new List<StepDetails>(); static double _millisecsPerTick = 1000.0 / Stopwatch.Frequency; } /// <summary> /// Node used in the hierarchical tree of timings used by the Profiler. /// Each node corresponds to a single line viewable in a UI-based representation. /// </summary> public class ProfileNode { /// <summary> /// The key for the node corresponds to the printable name of the callstack element. /// </summary> public readonly string key; #pragma warning disable 0649 /// <summary> /// Horribly hacky field only used by ink unity integration, /// but saves constructing an entire data structure that mirrors /// the one in here purely to store the state of whether each /// node in the UI has been opened or not /// </summary> public bool openInUI; #pragma warning restore 0649 /// <summary> /// Whether this node contains any sub-nodes - i.e. does it call anything else /// that has been recorded? /// </summary> /// <value><c>true</c> if has children; otherwise, <c>false</c>.</value> public bool hasChildren { get { return _nodes != null && _nodes.Count > 0; } } /// <summary> /// Total number of milliseconds this node has been active for. /// </summary> public int totalMillisecs { get { return (int)_totalMillisecs; } } public ProfileNode() { } public ProfileNode(string key) { this.key = key; } public void AddSample(string[] stack, double duration) { AddSample(stack, -1, duration); } void AddSample(string[] stack, int stackIdx, double duration) { _totalSampleCount++; _totalMillisecs += duration; if( stackIdx == stack.Length-1 ) { _selfSampleCount++; _selfMillisecs += duration; } if( stackIdx+1 < stack.Length ) AddSampleToNode(stack, stackIdx+1, duration); } void AddSampleToNode(string[] stack, int stackIdx, double duration) { var nodeKey = stack[stackIdx]; if( _nodes == null ) _nodes = new Dictionary<string, ProfileNode>(); ProfileNode node; if( !_nodes.TryGetValue(nodeKey, out node) ) { node = new ProfileNode(nodeKey); _nodes[nodeKey] = node; } node.AddSample(stack, stackIdx, duration); } /// <summary> /// Returns a sorted enumerable of the nodes in descending order of /// how long they took to run. /// </summary> public IEnumerable<KeyValuePair<string, ProfileNode>> descendingOrderedNodes { get { if( _nodes == null ) return null; return _nodes.OrderByDescending(keyNode => keyNode.Value._totalMillisecs); } } void PrintHierarchy(StringBuilder sb, int indent) { Pad(sb, indent); sb.Append(key); sb.Append(": "); sb.AppendLine(ownReport); if( _nodes == null ) return; foreach(var keyNode in descendingOrderedNodes) { keyNode.Value.PrintHierarchy(sb, indent+1); } } /// <summary> /// Generates a string giving timing information for this single node, including /// total milliseconds spent on the piece of ink, the time spent within itself /// (v.s. spent in children), as well as the number of samples (instruction steps) /// recorded for both too. /// </summary> /// <value>The own report.</value> public string ownReport { get { var sb = new StringBuilder(); sb.Append("total "); sb.Append(Profiler.FormatMillisecs(_totalMillisecs)); sb.Append(", self "); sb.Append(Profiler.FormatMillisecs(_selfMillisecs)); sb.Append(" ("); sb.Append(_selfSampleCount); sb.Append(" self samples, "); sb.Append(_totalSampleCount); sb.Append(" total)"); return sb.ToString(); } } void Pad(StringBuilder sb, int spaces) { for(int i=0; i<spaces; i++) sb.Append(" "); } /// <summary> /// String is a report of the sub-tree from this node, but without any of the header information /// that's prepended by the Profiler in its Report() method. /// </summary> public override string ToString () { var sb = new StringBuilder(); PrintHierarchy(sb, 0); return sb.ToString(); } Dictionary<string, ProfileNode> _nodes; double _selfMillisecs; double _totalMillisecs; int _selfSampleCount; int _totalSampleCount; } }