//#define LOGGING #if UNITY_4_6 || UNITY_4_7 || UNITY_5_0 || UNITY_5_1 #define LEGACY_UI #endif namespace SRDebugger.UI.Controls { using System.Collections.Generic; using Services; using SRF; using SRF.Service; using UnityEngine; using UnityEngine.UI; [ExecuteInEditMode] [RequireComponent(typeof (RectTransform))] [RequireComponent(typeof (CanvasRenderer))] public class ProfilerGraphControl : Graphic { public enum VerticalAlignments { Top, Bottom } public VerticalAlignments VerticalAlignment = VerticalAlignments.Bottom; private static readonly float[] ScaleSteps = { 1f/200f, 1f/160f, 1f/120f, 1f/100f, 1f/60f, 1f/30f, 1f/20f, 1f/12f, 1f/6f }; /// /// Resize the y-axis to fit the nearest useful fps value /// public bool FloatingScale; /// /// If not using FloatingScale, use the target FPS set by Application.targetFrameRate for TargetFps /// public bool TargetFpsUseApplication; /// /// Toggle drawing of the various axes /// public bool DrawAxes = true; /// /// If FloatingScale is disabled, use this value to determine y-axis /// public int TargetFps = 60; public bool Clip = true; public const float DataPointMargin = 2f; public const float DataPointVerticalMargin = 2f; public const float DataPointWidth = 4f; public int VerticalPadding = 10; public const int LineCount = 3; public Color[] LineColours = new Color[0]; private IProfilerService _profilerService; private ProfilerGraphAxisLabel[] _axisLabels; private Rect _clipBounds; #if LEGACY_UI private List _vbo; #else private readonly List _meshVertices = new List(); private readonly List _meshVertexColors = new List(); private readonly List _meshTriangles = new List(); #endif protected override void Awake() { base.Awake(); _profilerService = SRServiceManager.GetService(); } protected override void Start() { base.Start(); } protected void Update() { SetVerticesDirty(); } #if LEGACY_UI protected override void OnFillVBO(List vbo) #else [System.ObsoleteAttribute] protected override void OnPopulateMesh(Mesh m) #endif { #if LEGACY_UI _vbo = vbo; #else _meshVertices.Clear(); _meshVertexColors.Clear(); _meshTriangles.Clear(); #endif #if LOGGING if(!Application.isPlaying) Debug.Log("Draw"); #endif var graphWidth = rectTransform.rect.width; var graphHeight = rectTransform.rect.height; _clipBounds = new Rect(0, 0, graphWidth, graphHeight); var targetFps = TargetFps; if (Application.isPlaying && TargetFpsUseApplication && Application.targetFrameRate > 0) { targetFps = Application.targetFrameRate; } var maxValue = 1f/targetFps; // Holds the index of the nearest 'useful' FPS step var fpsStep = -1; var maxFrameTime = FloatingScale ? CalculateMaxFrameTime() : 1f/targetFps; if (FloatingScale) { for (var i = 0; i < ScaleSteps.Length; i++) { var step = ScaleSteps[i]; if (maxFrameTime < step*1.1f) { maxValue = step; fpsStep = i; break; } } // Fall back on the largest one if (fpsStep < 0) { fpsStep = ScaleSteps.Length - 1; maxValue = ScaleSteps[fpsStep]; } } else { // Search for the next scale step after the user-provided step for (var i = 0; i < ScaleSteps.Length; i++) { var step = ScaleSteps[i]; if (maxFrameTime > step) { fpsStep = i; } } } var verticalScale = (graphHeight - (VerticalPadding*2))/maxValue; // Number of data points that can fit into the graph space var availableDataPoints = CalculateVisibleDataPointCount(); // Reallocate vertex array if insufficient length (or not yet created) var sampleCount = GetFrameBufferCurrentSize(); for (var i = 0; i < sampleCount; i++) { // Break loop if all visible data points have been drawn if (i >= availableDataPoints) { break; } // When using right-alignment, read from the end of the profiler buffer var frame = GetFrame(sampleCount - i - 1); // Left-hand x coord var lx = graphWidth - DataPointWidth*i - DataPointWidth - graphWidth/2f; DrawDataPoint(lx, verticalScale, frame); } if (DrawAxes) { if (!FloatingScale) { DrawAxis(maxValue, maxValue*verticalScale, GetAxisLabel(0)); } var axisCount = 2; var j = 0; if (!FloatingScale) { j++; } for (var i = fpsStep; i >= 0; --i) { if (j >= axisCount) { break; } DrawAxis(ScaleSteps[i], ScaleSteps[i]*verticalScale, GetAxisLabel(j)); ++j; } } #if !LEGACY_UI m.Clear(); m.SetVertices(_meshVertices); m.SetColors(_meshVertexColors); m.SetTriangles(_meshTriangles, 0); #endif } protected void DrawDataPoint(float xPosition, float verticalScale, ProfilerFrame frame) { // Right-hand x-coord var rx = Mathf.Min(_clipBounds.width/2f, xPosition + DataPointWidth - DataPointMargin); var currentLineHeight = 0f; for (var j = 0; j < LineCount; j++) { var lineIndex = j; var value = 0f; if (j == 0) { value = (float) frame.UpdateTime; } else if (j == 1) { value = (float) frame.RenderTime; } else if (j == 2) { value = (float) frame.OtherTime; } value *= verticalScale; if (value.ApproxZero() || value - DataPointVerticalMargin*2f < 0f) { continue; } // Lower y-coord var ly = currentLineHeight + DataPointVerticalMargin - rectTransform.rect.height/2f; if (VerticalAlignment == VerticalAlignments.Top) { ly = rectTransform.rect.height/2f - currentLineHeight - DataPointVerticalMargin; } // Upper y-coord var uy = ly + value - DataPointVerticalMargin; if (VerticalAlignment == VerticalAlignments.Top) { uy = ly - value + DataPointVerticalMargin; } var c = LineColours[lineIndex]; AddRect(new Vector3(Mathf.Max(-_clipBounds.width/2f, xPosition), ly), new Vector3(Mathf.Max(-_clipBounds.width/2f, xPosition), uy), new Vector3(rx, uy), new Vector3(rx, ly), c); currentLineHeight += value; } } protected void DrawAxis(float frameTime, float yPosition, ProfilerGraphAxisLabel label) { #if LOGGING if(!Application.isPlaying) Debug.Log("Draw Axis: {0}".Fmt(yPosition)); #endif var lx = -rectTransform.rect.width*0.5f; var rx = -lx; var uy = yPosition - rectTransform.rect.height*0.5f + 0.5f; var ly = yPosition - rectTransform.rect.height*0.5f - 0.5f; var c = new Color(1f, 1f, 1f, 0.4f); AddRect(new Vector3(lx, ly), new Vector3(lx, uy), new Vector3(rx, uy), new Vector3(rx, ly), c); if (label != null) { label.SetValue(frameTime, yPosition); } } protected void AddRect(Vector3 tl, Vector3 tr, Vector3 bl, Vector3 br, Color c) { #if LEGACY_UI var v = UIVertex.simpleVert; v.color = c; v.position = tl; _vbo.Add(v); v.position = tr; _vbo.Add(v); v.position = bl; _vbo.Add(v); v.position = br; _vbo.Add(v); #else // New UI system uses triangles _meshVertices.Add(tl); _meshVertices.Add(tr); _meshVertices.Add(bl); _meshVertices.Add(br); _meshTriangles.Add(_meshVertices.Count - 4); // tl _meshTriangles.Add(_meshVertices.Count - 3); // tr _meshTriangles.Add(_meshVertices.Count - 1); // br _meshTriangles.Add(_meshVertices.Count - 2); // bl _meshTriangles.Add(_meshVertices.Count - 1); // br _meshTriangles.Add(_meshVertices.Count - 3); // tr _meshVertexColors.Add(c); _meshVertexColors.Add(c); _meshVertexColors.Add(c); _meshVertexColors.Add(c); #endif } protected ProfilerFrame GetFrame(int i) { #if UNITY_EDITOR if (!Application.isPlaying) { return TestData[i]; } #endif return _profilerService.FrameBuffer[i]; } protected int CalculateVisibleDataPointCount() { return Mathf.RoundToInt(rectTransform.rect.width/DataPointWidth); } protected int GetFrameBufferCurrentSize() { #if UNITY_EDITOR if (!Application.isPlaying) { return TestData.Length; } #endif return _profilerService.FrameBuffer.Count; } protected int GetFrameBufferMaxSize() { #if UNITY_EDITOR if (!Application.isPlaying) { return TestData.Length; } #endif return _profilerService.FrameBuffer.Capacity; } protected float CalculateMaxFrameTime() { var frameCount = GetFrameBufferCurrentSize(); var c = Mathf.Min(CalculateVisibleDataPointCount(), frameCount); var max = 0d; for (var i = 0; i < c; i++) { var frameNumber = frameCount - i - 1; var t = GetFrame(frameNumber); if (t.FrameTime > max) { max = t.FrameTime; } } return (float) max; } private ProfilerGraphAxisLabel GetAxisLabel(int index) { if (_axisLabels == null || !Application.isPlaying) { _axisLabels = GetComponentsInChildren(); } if (_axisLabels.Length > index) { return _axisLabels[index]; } Debug.LogWarning("[SRDebugger.Profiler] Not enough axis labels in pool"); return null; } #region Editor Only test data #if UNITY_EDITOR private ProfilerFrame[] TestData { get { if (_testData == null) { _testData = GenerateSampleData(); } return _testData; } } private ProfilerFrame[] _testData; protected static ProfilerFrame[] GenerateSampleData() { var sampleCount = 200; var data = new ProfilerFrame[sampleCount]; for (var i = 0; i < sampleCount; i++) { var frame = new ProfilerFrame(); for (var j = 0; j < 3; j++) { var v = 0d; if (j == 0) { v = Mathf.PerlinNoise(i/200f, 0); } else if (j == 1) { v = Mathf.PerlinNoise(0, i/200f); } else { v = Random.Range(0, 1f); } v *= (1f/60f)*0.333f; // Simulate spikes if (Random.value > 0.8f) { v *= Random.Range(1.2f, 1.8f); } if (j == 2) { v *= 0.1f; } if (j == 0) { frame.UpdateTime = v; } else if (j == 1) { frame.RenderTime = v; } else if (j == 2) { frame.FrameTime = frame.RenderTime + frame.UpdateTime + v; } } data[i] = frame; } data[0] = new ProfilerFrame { FrameTime = 0.005, RenderTime = 0.005, UpdateTime = 0.005 }; data[sampleCount - 1] = new ProfilerFrame { FrameTime = 1d/60d, RenderTime = 0.007, UpdateTime = 0.007 }; return data; } #endif #endregion } }