using System; using System.Collections.Generic; using UnityEngine; namespace UnityEditor.Performance.ProfileAnalyzer { [Serializable] internal class ProfileAnalysis { FrameSummary m_FrameSummary = new FrameSummary(); List m_Markers = new List(); List m_Threads = new List(); public ProfileAnalysis() { m_FrameSummary.first = 0; m_FrameSummary.last = 0; m_FrameSummary.count = 0; m_FrameSummary.msTotal = 0.0; m_FrameSummary.msMin = float.MaxValue; m_FrameSummary.msMax = 0.0f; m_FrameSummary.minFrameIndex = 0; m_FrameSummary.maxFrameIndex = 0; m_FrameSummary.maxMarkerDepth = 0; m_FrameSummary.totalMarkers = 0; m_FrameSummary.markerCountMax = 0; m_FrameSummary.markerCountMaxMean = 0.0f; for (int b = 0; b < m_FrameSummary.buckets.Length; b++) m_FrameSummary.buckets[b] = 0; m_Markers.Clear(); m_Threads.Clear(); } public void SetRange(int firstFrameIndex, int lastFrameIndex) { m_FrameSummary.first = firstFrameIndex; m_FrameSummary.last = lastFrameIndex; // Ensure these are initialized to frame indices within the range m_FrameSummary.minFrameIndex = firstFrameIndex; // if this wasn't initialized, and all frames had 0 length, it wouldn't be set in the UpdateSummary step of the analysis and point out of range m_FrameSummary.maxFrameIndex = firstFrameIndex; } public void AddMarker(MarkerData marker) { m_Markers.Add(marker); } public void AddThread(ThreadData thread) { m_Threads.Add(thread); } public void UpdateSummary(int frameIndex, float msFrame) { m_FrameSummary.msTotal += msFrame; m_FrameSummary.count += 1; if (msFrame < m_FrameSummary.msMin) { m_FrameSummary.msMin = msFrame; m_FrameSummary.minFrameIndex = frameIndex; } if (msFrame > m_FrameSummary.msMax) { m_FrameSummary.msMax = msFrame; m_FrameSummary.maxFrameIndex = frameIndex; } m_FrameSummary.frames.Add(new FrameTime(frameIndex, msFrame, 1)); } FrameTime GetPercentageOffset(List frames, float percent, out int outputFrameIndex) { int index = (int)((frames.Count - 1) * percent / 100); outputFrameIndex = frames[index].frameIndex; // True median is half of the sum of the middle 2 frames for an even count. However this would be a value never recorded so we avoid that. return frames[index]; } float GetThreadPercentageOffset(List frames, float percent, out int outputFrameIndex) { int index = (int)((frames.Count - 1) * percent / 100); outputFrameIndex = frames[index].frameIndex; // True median is half of the sum of the middle 2 frames for an even count. However this would be a value never recorded so we avoid that. return frames[index].ms; } void CalculateStandardDeviations(MarkerData marker) { if (marker.frames.Count <= 1) { marker.msStandardDeviation = 0; marker.countStandardDeviation = 0; return; } int frameCount = marker.frames.Count; float msMean = marker.msMean; float countMean = marker.countMean; double msSum = 0.0; double countSum = 0.0; for (int i = 0; i < frameCount; ++i) { float delta = (marker.frames[i].ms - msMean); msSum += (delta * delta); delta = (marker.frames[i].count - countMean); countSum += (delta * delta); } double variance = msSum / (frameCount - 1); marker.msStandardDeviation = (float)Math.Sqrt(variance); variance = countSum / (frameCount - 1); marker.countStandardDeviation = (float)Math.Sqrt(variance); } public void SetupMarkers() { int countMax = 0; float countMaxMean = 0.0f; foreach (MarkerData marker in m_Markers) { marker.msAtMedian = 0.0f; marker.msMin = float.MaxValue; marker.msMax = float.MinValue; marker.minFrameIndex = 0; marker.maxFrameIndex = 0; marker.countMin = int.MaxValue; marker.countMax = int.MinValue; foreach (FrameTime frameTime in marker.frames) { var ms = frameTime.ms; int frameIndex = frameTime.frameIndex; // Total time for marker over frame if (ms < marker.msMin) { marker.msMin = ms; marker.minFrameIndex = frameIndex; } if (ms > marker.msMax) { marker.msMax = ms; marker.maxFrameIndex = frameIndex; } if (frameIndex == m_FrameSummary.medianFrameIndex) marker.msAtMedian = ms; var count = frameTime.count; // count for marker over frame if (count < marker.countMin) { marker.countMin = count; } if (count > marker.countMax) { marker.countMax = count; } } int unusedIndex; marker.msMean = marker.presentOnFrameCount > 0 ? (float)(marker.msTotal / marker.presentOnFrameCount) : 0f; marker.frames.Sort(FrameTime.CompareCount); marker.countMedian = GetPercentageOffset(marker.frames, 50, out marker.medianFrameIndex).count; marker.countLowerQuartile = GetPercentageOffset(marker.frames, 25, out unusedIndex).count; marker.countUpperQuartile = GetPercentageOffset(marker.frames, 75, out unusedIndex).count; marker.countMean = marker.presentOnFrameCount > 0 ? (float)marker.count / marker.presentOnFrameCount : 0f; marker.frames.Sort(FrameTime.CompareMs); marker.msMedian = GetPercentageOffset(marker.frames, 50, out marker.medianFrameIndex).ms; marker.msLowerQuartile = GetPercentageOffset(marker.frames, 25, out unusedIndex).ms; marker.msUpperQuartile = GetPercentageOffset(marker.frames, 75, out unusedIndex).ms; CalculateStandardDeviations(marker); if (marker.countMax > countMax) countMax = marker.countMax; if (marker.countMean > countMaxMean) countMaxMean = marker.countMean; } m_FrameSummary.markerCountMax = countMax; m_FrameSummary.markerCountMaxMean = countMaxMean; } public void SetupMarkerBuckets() { // using a for loop instead of foreach is surprisingly faster on Mono for (int i = 0, n = m_Markers.Count; i < n; i++) { var marker = m_Markers[i]; marker.ComputeBuckets(marker.msMin, marker.msMax); marker.ComputeCountBuckets(marker.countMin, marker.countMax); } } public void SetupFrameBuckets(float timeScaleMax) { float first = 0; float last = timeScaleMax; float range = last - first; int maxBucketIndex = m_FrameSummary.buckets.Length - 1; for (int bucketIndex = 0; bucketIndex < m_FrameSummary.buckets.Length; bucketIndex++) { m_FrameSummary.buckets[bucketIndex] = 0; } float scale = range > 0 ? m_FrameSummary.buckets.Length / range : 0; // using a for loop instead of foreach is surprisingly faster on Mono for (int i = 0, n = m_FrameSummary.frames.Count; i < n; i++) { var frameData = m_FrameSummary.frames[i]; var msFrame = frameData.ms; //var frameIndex = frameData.frameIndex; int bucketIndex = (int)((msFrame - first) * scale); if (bucketIndex < 0 || bucketIndex > maxBucketIndex) { // It can occur for the highest entry in the range (max-min/range) = 1 // if (ms > max) // Check for the spilling case // Debug.Log(string.Format("Frame {0}ms exceeds range {1}-{2} on frame {3}", msFrame, first, last, frameIndex)); if (bucketIndex > maxBucketIndex) bucketIndex = maxBucketIndex; else bucketIndex = 0; } m_FrameSummary.buckets[bucketIndex] += 1; } if (range == 0) { // All buckets will be the same for (int bucketIndex = 1; bucketIndex < m_FrameSummary.buckets.Length; bucketIndex++) { m_FrameSummary.buckets[bucketIndex] = m_FrameSummary.buckets[0]; } } } void CalculateThreadMedians() { foreach (var thread in m_Threads) { if (thread.frames.Count > 0) { thread.frames.Sort(); int unusedIndex; thread.msMin = GetThreadPercentageOffset(thread.frames, 0, out thread.minFrameIndex); thread.msLowerQuartile = GetThreadPercentageOffset(thread.frames, 25, out unusedIndex); thread.msMedian = GetThreadPercentageOffset(thread.frames, 50, out thread.medianFrameIndex); thread.msUpperQuartile = GetThreadPercentageOffset(thread.frames, 75, out unusedIndex); thread.msMax = GetThreadPercentageOffset(thread.frames, 100, out thread.maxFrameIndex); // Put back in order of frames thread.frames.Sort((a, b) => a.frameIndex.CompareTo(b.frameIndex)); } else { thread.msMin = 0f; thread.msLowerQuartile = 0f; thread.msMedian = 0f; thread.msUpperQuartile = 0f; thread.msMax = 0f; } } } public void Finalise(float timeScaleMax, int maxMarkerDepth) { if (m_FrameSummary.frames.Count > 0) { m_FrameSummary.frames.Sort(); m_FrameSummary.msMean = (float)(m_FrameSummary.msTotal / m_FrameSummary.count); m_FrameSummary.msMedian = GetPercentageOffset(m_FrameSummary.frames, 50, out m_FrameSummary.medianFrameIndex).ms; int unusedIndex; m_FrameSummary.msLowerQuartile = GetPercentageOffset(m_FrameSummary.frames, 25, out unusedIndex).ms; m_FrameSummary.msUpperQuartile = GetPercentageOffset(m_FrameSummary.frames, 75, out unusedIndex).ms; } else { m_FrameSummary.msMean = 0f; m_FrameSummary.msMedian = 0f; m_FrameSummary.msLowerQuartile = 0f; m_FrameSummary.msUpperQuartile = 0f; // This started as float.MaxValue and won't have been updated m_FrameSummary.msMin = 0f; } // No longer need the frame time list ? //m_frameSummary.msFrame.Clear(); m_FrameSummary.maxMarkerDepth = maxMarkerDepth; if (timeScaleMax <= 0.0f) { // If max frame time range not specified then use the max frame value found. timeScaleMax = m_FrameSummary.msMax; } else if (timeScaleMax < m_FrameSummary.msMax) { Debug.Log(string.Format("Expanding timeScaleMax {0} to match max value found {1}", timeScaleMax, m_FrameSummary.msMax)); // If max frame time range too small we must expand it. timeScaleMax = m_FrameSummary.msMax; } SetupMarkers(); SetupMarkerBuckets(); SetupFrameBuckets(timeScaleMax); // Sort in median order (highest first) m_Markers.Sort(SortByAtMedian); CalculateThreadMedians(); } int SortByAtMedian(MarkerData a, MarkerData b) { if (a.msAtMedian == b.msAtMedian) return -a.medianFrameIndex.CompareTo(b.medianFrameIndex); return -a.msAtMedian.CompareTo(b.msAtMedian); } public List GetMarkers() { return m_Markers; } public List GetThreads() { return m_Threads; } public ThreadData GetThreadByName(string threadNameWithIndex) { foreach (var thread in m_Threads) { if (thread.threadNameWithIndex == threadNameWithIndex) return thread; } return null; } public FrameSummary GetFrameSummary() { return m_FrameSummary; } public MarkerData GetMarker(int index) { if (index < 0 || index >= m_Markers.Count) return null; return m_Markers[index]; } public int GetMarkerIndexByName(string markerName) { if (markerName == null) return -1; for (int index = 0; index < m_Markers.Count; index++) { var marker = m_Markers[index]; if (marker.name == markerName) { return index; } } return -1; } public MarkerData GetMarkerByName(string markerName) { if (markerName == null) return null; for (int index = 0; index < m_Markers.Count; index++) { var marker = m_Markers[index]; if (marker.name == markerName) { return marker; } } return null; } } }