using UnityEngine; using System; using System.Collections.Generic; using UnityEngine.Profiling; namespace UnityEditor.Performance.ProfileAnalyzer { [Serializable] internal class FrameTimeGraphGlobalSettings { public bool showThreads = false; public bool showSelectedMarker = true; public bool showFrameLines = true; public bool showFrameLineText = true; public bool showOrderedByFrameDuration = false; } internal class FrameTimeGraph { static FrameTimeGraphGlobalSettings m_GlobalSettings = new FrameTimeGraphGlobalSettings(); static public void SetGlobalSettings(FrameTimeGraphGlobalSettings globalSettings) { m_GlobalSettings = globalSettings; } public struct Data { public readonly float ms; public readonly int frameOffset; public Data(float _ms, int _index) { ms = _ms; frameOffset = _index; } }; public delegate void SetRange(List selected, int clickCount, FrameTimeGraph.State inputStatus); public delegate void SetActive(bool active); public enum State { None, Dragging, DragComplete }; enum DragDirection { Start, Forward, Backward, None }; enum AxisMode { One60HzFrame, Two60HzFrames, Four60HzFrames, Max, Custom }; Draw2D m_2D; int m_DragBeginFirstOffset; int m_DragBeginLastOffset; bool m_Dragging; int m_DragFirstOffset; int m_DragLastOffset; bool m_Moving; int m_MoveHandleOffset; bool m_SingleControlAction; int m_ClickCount; double m_LastClickTime; bool m_MouseReleased; bool m_Zoomed; int m_ZoomStartOffset; int m_ZoomEndOffset; Color m_ColorBarBackground; Color m_ColorBarBackgroundSelected; Color m_ColorBar; Color m_ColorBarOutOfRange; Color m_ColorBarSelected; Color m_ColorBarThreads; Color m_ColorBarThreadsOutOfRange; Color m_ColorBarThreadsSelected; Color m_ColorBarMarker; Color m_ColorBarMarkerOutOfRange; Color m_ColorBarMarkerSelected; Color m_ColorGridLine; FrameTimeGraph m_PairedWithFrameTimeGraph; internal static class Styles { public static readonly GUIContent menuItemClearSelection = new GUIContent("Clear Selection"); public static readonly GUIContent menuItemSelectAll = new GUIContent("Select All"); public static readonly GUIContent menuItemInvertSelection = new GUIContent("Invert Selection"); public static readonly GUIContent menuItemZoomSelection = new GUIContent("Zoom Selection"); public static readonly GUIContent menuItemZoomAll = new GUIContent("Zoom All"); public static readonly GUIContent menuItemSelectMin = new GUIContent("Select Shortest Frame"); public static readonly GUIContent menuItemSelectMax = new GUIContent("Select Longest Frame"); public static readonly GUIContent menuItemSelectMedian = new GUIContent("Select Median Frame"); public static readonly GUIContent menuItemSelectPrevious = new GUIContent("Move selection left _LEFT"); public static readonly GUIContent menuItemSelectNext = new GUIContent("Move selection right _RIGHT"); public static readonly GUIContent menuItemSelectGrow = new GUIContent("Grow selection _="); public static readonly GUIContent menuItemSelectShrink = new GUIContent("Shrink selection _-"); public static readonly GUIContent menuItemSelectGrowLeft = new GUIContent("Grow selection left _<"); public static readonly GUIContent menuItemSelectGrowRight = new GUIContent("Grow selection right _>"); public static readonly GUIContent menuItemSelectShrinkLeft = new GUIContent("Shrink selection left _&<"); public static readonly GUIContent menuItemSelectShrinkRight = new GUIContent("Shrink selection right _&>"); public static readonly GUIContent menuItemSelectGrowFast = new GUIContent("Grow selection (fast) _#="); public static readonly GUIContent menuItemSelectShrinkFast = new GUIContent("Shrink selection (fast) _#-"); public static readonly GUIContent menuItemSelectGrowLeftFast = new GUIContent("Grow selection left (fast) _#<"); public static readonly GUIContent menuItemSelectGrowRightFast = new GUIContent("Grow selection right (fast) _#>"); public static readonly GUIContent menuItemSelectShrinkLeftFast = new GUIContent("Shrink selection left (fast) _#&<"); public static readonly GUIContent menuItemSelectShrinkRightFast = new GUIContent("Shrink selection right (fast) _#&>"); public static readonly GUIContent menuItemShowSelectedMarker = new GUIContent("Show Selected Marker"); public static readonly GUIContent menuItemShowThreads = new GUIContent("Show Filtered Threads"); // public static readonly GUIContent menuItemDetailedMode = new GUIContent("Detailed mode"); public static readonly GUIContent menuItemShowFrameLines = new GUIContent("Show Frame Lines"); public static readonly GUIContent menuItemShowFrameLineText = new GUIContent("Show Frame Line Text"); public static readonly GUIContent menuItemShowOrderedByFrameDuration = new GUIContent("Order by Frame Duration"); } const int kXAxisWidth = 80; const int kYAxisDetailThreshold = 40; const int kOverrunHeight = 3; static AxisMode s_YAxisMode; static float m_YAxisMs; bool m_IsOrderedByFrameDuration; List m_Values = new List {}; List m_LastSelectedFrameOffsets = new List {}; int[] m_FrameOffsetToDataOffsetMapping = new int[] {}; SetRange m_SetRange; SetActive m_SetActive; List m_CurrentSelection = new List(); int m_CurrentSelectionFirstDataOffset; int m_CurrentSelectionLastDataOffset; int m_GraphId; int m_ControlID; static int s_LastSelectedGraphId = -1; static int s_CurrentSelectedGraphId = -1; bool m_Enabled; struct BarData { public float x; public float y; public float w; public float h; public int startDataOffset; public int endDataOffset; public float yMin; public float yMax; public BarData(float _x, float _y, float _w, float _h, int _startDataOffset, int _endDataOffset, float _yMin, float _yMax) { x = _x; y = _y; w = _w; h = _h; startDataOffset = _startDataOffset; endDataOffset = _endDataOffset; yMin = _yMin; yMax = _yMax; } } List m_Bars = new List(); DisplayUnits m_Units; Rect m_LastRect; int m_MaxFrames; string DisplayUnits() { return m_Units.Postfix(); } string ToDisplayUnits(float ms, bool showUnits = false, int limitToNDigits = 5) { return m_Units.ToString(ms, showUnits, limitToNDigits); } public void SetUnits(Units units) { m_Units = new DisplayUnits(units); } public void Reset() { m_Zoomed = false; m_Dragging = false; ClearDragSelection(); m_Moving = false; m_ClickCount = 0; m_MouseReleased = false; } void Init() { Reset(); m_PairedWithFrameTimeGraph = null; m_YAxisMs = 100f; s_YAxisMode = AxisMode.Max; m_IsOrderedByFrameDuration = false; m_Enabled = true; m_LastRect = new Rect(0, 0, 0, 0); m_MaxFrames = -1; } public void MakeGraphActive(bool activate) { if (activate) { if (s_CurrentSelectedGraphId != m_GraphId) { s_LastSelectedGraphId = m_GraphId; s_CurrentSelectedGraphId = m_GraphId; // Make sure we are not still selecting another graph GUIUtility.hotControl = 0; m_SetActive(true); } if (GUI.GetNameOfFocusedControl() != "FrameTimeGraph") { // Take focus away from any other control // Doesn't really matter what the name is here GUI.FocusControl("FrameTimeGraph"); } } else { if (s_CurrentSelectedGraphId == m_GraphId) { s_CurrentSelectedGraphId = -1; // Remember this was the active control // Before this point one of the inner labels would have been active // GUIUtility.hotControl = m_ControlID; m_SetActive(false); } } } public bool IsGraphActive() { if (s_CurrentSelectedGraphId == m_GraphId) return true; if (s_LastSelectedGraphId == m_GraphId && GUIUtility.hotControl == m_ControlID) return true; return false; } public FrameTimeGraph(int graphID, Draw2D draw2D, Units units, Color background, Color backgroundSelected, Color barColor, Color barSelected, Color barMarker, Color barMarkerSelected, Color barThreads, Color barThreadsSelected, Color colorGridlines) { m_GraphId = graphID; m_ControlID = 0; m_2D = draw2D; SetUnits(units); Init(); float ratio = 0.75f; m_ColorBarBackground = background; m_ColorBarBackgroundSelected = backgroundSelected; m_ColorBar = barColor; m_ColorBarOutOfRange = new Color(barColor.r * ratio, barColor.g * ratio, barColor.b * ratio); m_ColorBarSelected = barSelected; m_ColorBarMarker = barMarker; m_ColorBarMarkerOutOfRange = new Color(barMarker.r * ratio, barMarker.g * ratio, barMarker.b * ratio); m_ColorBarMarkerSelected = barMarkerSelected; m_ColorBarThreads = barThreads; m_ColorBarThreadsOutOfRange = new Color(barThreads.r * ratio, barThreads.g * ratio, barThreads.b * ratio); m_ColorBarThreadsSelected = barThreadsSelected; m_ColorGridLine = colorGridlines; } int ClampToRange(int value, int min, int max) { if (value < min) value = min; if (value > max) value = max; return value; } int GetDataOffsetForXUnclamped(int xPosition, int width, int totalDataSize) { int visibleDataSize; if (m_Zoomed) visibleDataSize = (m_ZoomEndOffset - m_ZoomStartOffset) + 1; else visibleDataSize = totalDataSize; int dataOffset = (int)(xPosition * visibleDataSize / width); if (m_Zoomed) dataOffset += m_ZoomStartOffset; return dataOffset; } int GetDataOffsetForX(int xPosition, int width, int totalDataSize) { //xPosition = ClampToRange(xPosition, 0, width-1); int dataOffset = GetDataOffsetForXUnclamped(xPosition, width, totalDataSize); return ClampToRange(dataOffset, 0, totalDataSize - 1); } int GetXForDataOffset(int dataOffset, int width, int totalDataSize) { //frameOffset = ClampToRange(frameOffset, 0, frames-1); int visibleDataSize; if (m_Zoomed) { dataOffset = ClampToRange(dataOffset, m_ZoomStartOffset, m_ZoomEndOffset + 1); dataOffset -= m_ZoomStartOffset; visibleDataSize = (m_ZoomEndOffset - m_ZoomStartOffset) + 1; } else visibleDataSize = totalDataSize; int x = (int)(dataOffset * width / visibleDataSize); x = ClampToRange(x, 0, width - 1); return x; } void SetDragMovement(int startOffset, int endOffset, int currentSelectionFirstDataOffset, int currentSelectionLastDataOffset) { // Maintain length but clamp to range int frames = m_Values.Count; int currentSelectionRange = currentSelectionLastDataOffset - currentSelectionFirstDataOffset; endOffset = startOffset + currentSelectionRange; startOffset = ClampToRange(startOffset, 0, frames - (currentSelectionRange + 1)); endOffset = ClampToRange(endOffset, 0, frames - 1); SetDragSelection(startOffset, endOffset); if (m_PairedWithFrameTimeGraph != null && !m_SingleControlAction) { m_PairedWithFrameTimeGraph.SetDragSelection(m_DragFirstOffset, m_DragLastOffset); } } void SetDragSelection(int startOffset, int endOffset, DragDirection dragDirection) { // No need to clamp these as input is clamped. switch (dragDirection) { case DragDirection.Forward: SetDragSelection(m_DragBeginFirstOffset, endOffset); break; case DragDirection.Backward: SetDragSelection(startOffset, m_DragBeginLastOffset); break; case DragDirection.Start: SetDragSelection(startOffset, endOffset); // Record first selected bar range m_DragBeginFirstOffset = m_DragFirstOffset; m_DragBeginLastOffset = m_DragLastOffset; break; } if (m_PairedWithFrameTimeGraph != null && !m_SingleControlAction) { m_PairedWithFrameTimeGraph.SetDragSelection(m_DragFirstOffset, m_DragLastOffset); } } public void SetDragSelection(int startOffset, int endOffset) { m_DragFirstOffset = startOffset; m_DragLastOffset = endOffset; } public void ClearDragSelection() { m_DragFirstOffset = -1; m_DragLastOffset = -1; } public bool HasDragRegion() { return (m_DragFirstOffset != -1); } public void GetSelectedRange(List frameOffsets, out int firstDataOffset, out int lastDataOffset, out int firstFrameOffset, out int lastFrameOffset) { int frames = m_Values != null ? m_Values.Count : 0; firstDataOffset = 0; lastDataOffset = frames - 1; firstFrameOffset = 0; lastFrameOffset = frames - 1; if (m_FrameOffsetToDataOffsetMapping.Length > 0) { // By default data is ordered by index so first/last will be the selected visible range if (frameOffsets.Count >= 1) { firstFrameOffset = frameOffsets[0]; lastFrameOffset = firstFrameOffset; firstDataOffset = GetDataOffset(firstFrameOffset); lastDataOffset = firstDataOffset; } if (frameOffsets.Count >= 2) { lastFrameOffset = frameOffsets[frameOffsets.Count - 1]; lastDataOffset = GetDataOffset(lastFrameOffset); } if (m_GlobalSettings.showOrderedByFrameDuration) { // Need to find the selected items with lowest and highest ms values if (frameOffsets.Count > 0) { int dataOffset = GetDataOffset(firstFrameOffset); firstDataOffset = dataOffset; lastDataOffset = dataOffset; float firstDataMS = m_Values[dataOffset].ms; float lastDataMS = m_Values[dataOffset].ms; foreach (int frameOffset in frameOffsets) { dataOffset = GetDataOffset(frameOffset); float ms = m_Values[dataOffset].ms; if (ms <= firstDataMS && dataOffset < firstDataOffset) { firstDataMS = ms; firstDataOffset = dataOffset; } if (ms >= lastDataMS && dataOffset > lastDataOffset) { lastDataMS = ms; lastDataOffset = dataOffset; } } } } } } public bool IsMultiSelectControlHeld() { #if UNITY_EDITOR_OSX return Event.current.command; #else return Event.current.control; #endif } public State ProcessInput() { if (!IsEnabled()) return State.None; if (m_Values == null) return State.None; if (m_LastRect.width == 0 || m_MaxFrames < 0) return State.None; Rect rect = m_LastRect; int maxFrames = m_MaxFrames; int dataLength = m_Values.Count; if (dataLength <= 0) return State.None; if (m_IsOrderedByFrameDuration != m_GlobalSettings.showOrderedByFrameDuration) { // Reorder if necessary SetData(m_Values); } int currentSelectionFirstDataOffset; int currentSelectionLastDataOffset; int currentSelectionFirstFrameOffset; int currentSelectionLastFrameOffset; GetSelectedRange(m_LastSelectedFrameOffsets, out currentSelectionFirstDataOffset, out currentSelectionLastDataOffset, out currentSelectionFirstFrameOffset, out currentSelectionLastFrameOffset); m_CurrentSelection.Clear(); m_CurrentSelection.AddRange(m_LastSelectedFrameOffsets); m_CurrentSelectionFirstDataOffset = currentSelectionFirstDataOffset; m_CurrentSelectionLastDataOffset = currentSelectionLastDataOffset; if (Event.current.isKey && Event.current.type == EventType.KeyDown && !m_Dragging && !m_MouseReleased) { if (IsGraphActive()) { int step = Event.current.shift ? 10 : 1; var eventUsed = false; switch (Event.current.keyCode) { case KeyCode.LeftArrow: SelectPrevious(step); eventUsed = true; break; case KeyCode.RightArrow: SelectNext(step); eventUsed = true; break; case KeyCode.Less: case KeyCode.Comma: if (Event.current.alt) SelectShrinkLeft(step); else SelectGrowLeft(step); eventUsed = true; break; case KeyCode.Greater: case KeyCode.Period: if (Event.current.alt) SelectShrinkRight(step); else SelectGrowRight(step); eventUsed = true; break; case KeyCode.Plus: case KeyCode.Equals: case KeyCode.KeypadPlus: if (Event.current.alt) SelectShrink(step); else SelectGrow(step); eventUsed = true; break; case KeyCode.Underscore: case KeyCode.Minus: case KeyCode.KeypadMinus: if (Event.current.alt) SelectGrow(step); else SelectShrink(step); eventUsed = true; break; } if (eventUsed) Event.current.Use(); } } float doubleClickTimeout = 0.25f; if (m_MouseReleased) { if ((EditorApplication.timeSinceStartup - m_LastClickTime) > doubleClickTimeout) { // By this point we will know if its a single or double click bool append = IsMultiSelectControlHeld(); CallSetRange(m_DragFirstOffset, m_DragLastOffset, m_ClickCount, m_SingleControlAction, FrameTimeGraph.State.DragComplete, append); ClearDragSelection(); if (m_PairedWithFrameTimeGraph != null && !m_SingleControlAction) m_PairedWithFrameTimeGraph.ClearDragSelection(); m_MouseReleased = false; } } int width = (int)rect.width; int height = (int)rect.height; float xStart = rect.xMin; if (height > kYAxisDetailThreshold) { float h = GUI.skin.label.lineHeight; xStart += kXAxisWidth; width -= kXAxisWidth; } if (maxFrames > 0) { if (!m_Zoomed) width = width * dataLength / maxFrames; } // Process input Event e = Event.current; if (e.isMouse) { if (m_Dragging) { if (e.type == EventType.MouseUp) { m_Dragging = false; m_Moving = false; // Delay the action as we are checking for double click m_MouseReleased = true; return State.Dragging; } } int x = (int)(e.mousePosition.x - xStart); int dataOffset = GetDataOffsetForXUnclamped(x, width, dataLength); if (m_Moving) dataOffset -= m_MoveHandleOffset; dataOffset = ClampToRange(dataOffset, 0, dataLength - 1); int frameOffsetBeforeNext = Math.Max(dataOffset, GetDataOffsetForX(x + 1, width, dataLength) - 1); if (m_Dragging) { if (e.button == 0) { // Still dragging (doesn't have to be within the y bounds) if (m_Moving) { // Forward drag from start point SetDragMovement(dataOffset, frameOffsetBeforeNext, currentSelectionFirstDataOffset, currentSelectionLastDataOffset); } else { DragDirection dragDirection = (dataOffset < m_DragBeginFirstOffset) ? DragDirection.Backward : DragDirection.Forward; SetDragSelection(dataOffset, frameOffsetBeforeNext, dragDirection); } CallSetRange(m_DragFirstOffset, m_DragLastOffset, m_ClickCount, m_SingleControlAction, FrameTimeGraph.State.Dragging); return State.Dragging; } } else { if (e.mousePosition.x >= rect.x && e.mousePosition.x <= rect.xMax && e.mousePosition.y >= rect.y && e.mousePosition.y <= rect.yMax) { if (e.mousePosition.x >= xStart && e.mousePosition.x <= (xStart + width) && e.mousePosition.y >= rect.y && e.mousePosition.y < rect.yMax) { MakeGraphActive(true); if (e.type == EventType.MouseDown && e.button == 0) { // Drag start (must be within the bounds of the control) // Might be single or double click m_LastClickTime = EditorApplication.timeSinceStartup; m_ClickCount = e.clickCount; m_Dragging = true; m_Moving = false; if (currentSelectionFirstDataOffset != 0 || currentSelectionLastDataOffset != dataLength - 1) { // Selection is valid if (e.shift && dataOffset >= currentSelectionFirstDataOffset && frameOffsetBeforeNext <= currentSelectionLastDataOffset) { // Moving if shift held and we are inside the current selection range m_Moving = true; } } if (m_PairedWithFrameTimeGraph != null) m_SingleControlAction = e.alt; // Record if we are acting only on this control rather than the paired one too else m_SingleControlAction = true; if (m_Moving) { m_MoveHandleOffset = dataOffset - currentSelectionFirstDataOffset; SetDragMovement(currentSelectionFirstDataOffset, currentSelectionLastDataOffset, currentSelectionFirstDataOffset, currentSelectionLastDataOffset); } else { //SetDragSelection(dataOffset, frameOffsetBeforeNext, DragDirection.Start); // Select just 1 frame SetDragSelection(dataOffset, dataOffset, DragDirection.Start); } CallSetRange(m_DragFirstOffset, m_DragLastOffset, m_ClickCount, m_SingleControlAction, FrameTimeGraph.State.Dragging); return State.Dragging; } } } else { // Left this graph area MakeGraphActive(false); } } } if (m_MouseReleased) { // Not finished drag fully yet CallSetRange(m_DragFirstOffset, m_DragLastOffset, m_ClickCount, m_SingleControlAction, FrameTimeGraph.State.Dragging); return State.Dragging; } return State.None; } public float GetDataRange() { if (m_Values == null) return 0f; int frames = m_Values.Count; float min = 0f; float max = 0f; for (int frameOffset = 0; frameOffset < frames; frameOffset++) { float ms = m_Values[frameOffset].ms; if (ms > max) max = ms; } float hRange = max - min; return hRange; } public void PairWith(FrameTimeGraph otherFrameTimeGraph) { if (m_PairedWithFrameTimeGraph != null) { // Clear existing pairing m_PairedWithFrameTimeGraph.m_PairedWithFrameTimeGraph = null; } m_PairedWithFrameTimeGraph = otherFrameTimeGraph; if (otherFrameTimeGraph != null) otherFrameTimeGraph.m_PairedWithFrameTimeGraph = this; } public FrameTimeGraph GetPairedWith() { return m_PairedWithFrameTimeGraph; } public float GetYAxisRange(float yMax) { switch (s_YAxisMode) { case AxisMode.One60HzFrame: return 1000f / 60f; case AxisMode.Two60HzFrames: return 2000f / 60f; case AxisMode.Four60HzFrames: return 4000f / 60f; case AxisMode.Max: return yMax; case AxisMode.Custom: return m_YAxisMs; } return yMax; } public void SetData(List values) { if (values == null) return; m_Values = values; if (m_GlobalSettings.showOrderedByFrameDuration) m_Values.Sort((a, b) => { return a.ms.CompareTo(b.ms); }); else m_Values.Sort((a, b) => { return a.frameOffset.CompareTo(b.frameOffset); }); m_FrameOffsetToDataOffsetMapping = new int[m_Values.Count]; for (int dataOffset = 0; dataOffset < m_Values.Count; dataOffset++) m_FrameOffsetToDataOffsetMapping[m_Values[dataOffset].frameOffset] = dataOffset; m_CurrentSelection.Clear(); for (int frameIndex = 0; frameIndex < m_Values.Count; frameIndex++) { m_CurrentSelection.Add(frameIndex); } m_CurrentSelectionFirstDataOffset = 0; m_CurrentSelectionLastDataOffset = m_Values.Count - 1; m_IsOrderedByFrameDuration = m_GlobalSettings.showOrderedByFrameDuration; } int GetDataOffset(int frameOffset) { if (frameOffset < 0 || frameOffset >= m_FrameOffsetToDataOffsetMapping.Length) { Debug.Log(string.Format("{0} out of range of frame offset to data offset mapping {1}", frameOffset, m_FrameOffsetToDataOffsetMapping.Length)); return 0; } return m_FrameOffsetToDataOffsetMapping[frameOffset]; } public void ClearData() { m_Values = null; } public bool HasData() { if (m_Values == null) return false; if (m_Values.Count == 0) return false; return true; } public void SetActiveCallback(SetActive setActive) { m_SetActive = setActive; } public void SetRangeCallback(SetRange setRange) { m_SetRange = setRange; } void CallSetRange(int startDataOffset, int endDataOffset, int clickCount, bool singleControlAction, FrameTimeGraph.State inputStatus, bool append = false, bool effectPaired = true) { if (m_SetRange == null) return; if (startDataOffset < 0 && endDataOffset < 0) { // Clear m_SetRange(new List(), clickCount, inputStatus); return; } startDataOffset = Math.Max(0, startDataOffset); endDataOffset = Math.Min(endDataOffset, m_Values.Count - 1); List selected = new List(); if (append && m_LastSelectedFrameOffsets.Count != m_Values.Count) { foreach (int frameOffset in m_LastSelectedFrameOffsets) { int dataOffset = m_FrameOffsetToDataOffsetMapping[frameOffset]; if (dataOffset >= 0 && dataOffset < m_Values.Count) { selected.Add(frameOffset); } } } for (int dataOffset = startDataOffset; dataOffset <= endDataOffset; dataOffset++) { if (dataOffset >= 0 && dataOffset < m_Values.Count) { int frameOffset = m_Values[dataOffset].frameOffset; if (append == false || !selected.Contains(frameOffset)) { selected.Add(frameOffset); } } } // Sort selection in frame index order so start is lowest and end is highest selected.Sort(); if (selected.Count == 0) return; m_SetRange(selected, clickCount, inputStatus); if (m_PairedWithFrameTimeGraph != null && m_PairedWithFrameTimeGraph.m_Values.Count > 1 && effectPaired && !singleControlAction) { // Update selection on the other frame time graph int mainMaxFrame = m_Values.Count - 1; int otherMaxFrame = m_PairedWithFrameTimeGraph.m_Values.Count - 1; int startOffset = startDataOffset; int endOffset = endDataOffset; if (startOffset > otherMaxFrame) { if (append) { // Nothing more to do return; } // Select all, if the main selection is outsize the range of the other startOffset = 0; endOffset = otherMaxFrame; } else { if (startOffset == 0 && endOffset == mainMaxFrame) { // If clearing main selection then clear the other section fully too endOffset = otherMaxFrame; } startOffset = ClampToRange(startOffset, 0, otherMaxFrame); endOffset = ClampToRange(endOffset, 0, otherMaxFrame); } m_PairedWithFrameTimeGraph.CallSetRange(startOffset, endOffset, clickCount, singleControlAction, inputStatus, append, false); } } bool HasNoSelection() { if (m_Values == null) return false; int frames = m_Values.Count; return ((m_CurrentSelectionFirstDataOffset < 0 || m_CurrentSelectionLastDataOffset >= frames) && (m_CurrentSelectionLastDataOffset < 0 || m_CurrentSelectionLastDataOffset >= frames)); } bool HasSelectedAll() { if (m_Values == null) return false; int frames = m_Values.Count; return (m_CurrentSelectionFirstDataOffset == 0 && m_CurrentSelectionLastDataOffset == (frames - 1)); } bool HasSubsetSelected() { if (m_Values == null) return false; return !(HasSelectedAll() || HasNoSelection()); } void RegenerateBars(float x, float y, float width, float height, float yRange) { int frames = m_Values.Count; if (frames <= 0) return; m_Bars.Clear(); int nextDataOffset = GetDataOffsetForX(0, (int)width, frames); for (int barX = 0; barX < width; barX++) { int startDataOffset = nextDataOffset; nextDataOffset = GetDataOffsetForX(barX + 1, (int)width, frames); int endDataOffset = Math.Max(startDataOffset, nextDataOffset - 1); float min = m_Values[startDataOffset].ms; float max = min; for (int dataOffset = startDataOffset + 1; dataOffset <= endDataOffset; dataOffset++) { float ms = m_Values[dataOffset].ms; if (ms < min) min = ms; if (ms > max) max = ms; } float maxClamped = Math.Min(max, yRange); float h = height * maxClamped / yRange; m_Bars.Add(new BarData(x + barX, y, 1, h, startDataOffset, endDataOffset, min, max)); } } public void SetEnabled(bool enabled) { m_Enabled = enabled; } public bool IsEnabled() { return m_Enabled; } float GetTotalSelectionTime(List selectedFrameOffsets) { float totalMs = 0; for (int i = 0; i < selectedFrameOffsets.Count; i++) { int frameOffset = selectedFrameOffsets[i]; int dataOffset = m_FrameOffsetToDataOffsetMapping[frameOffset]; totalMs += m_Values[dataOffset].ms; } return totalMs; } float GetTotalSelectionTime(int firstOffset, int lastOffset) { float totalMs = 0; for (int frameOffset = firstOffset; frameOffset <= lastOffset; frameOffset++) { if (frameOffset < m_FrameOffsetToDataOffsetMapping.Length) { int dataOffset = m_FrameOffsetToDataOffsetMapping[frameOffset]; totalMs += m_Values[dataOffset].ms; } } return totalMs; } void ShowFrameLines(float x, float y, float yRange, float width, float height) { float msSegment = 1000f / 60f; int lines = (int)(yRange / msSegment); int step = 1; for (int line = 1; line <= lines; line += step, step *= 2) { float ms = line * msSegment; float h = height * ms / yRange; m_2D.DrawLine(x, y + h, x + width - 1, y + h, m_ColorGridLine); } } bool InSelectedRegion(int startDataOffset, int endDataOffset, int selectedFirstOffset, int selectedLastOffset, Dictionary frameOffsetToSelectionIndex, bool subsetSelected) { bool inSelectionRegion = false; bool showCurrentSelection = false; if (HasDragRegion()) { if (endDataOffset >= selectedFirstOffset && startDataOffset <= selectedLastOffset) { inSelectionRegion = true; } if (IsMultiSelectControlHeld() && m_LastSelectedFrameOffsets.Count != m_Values.Count) { // Show current selection too showCurrentSelection = true; } } else { showCurrentSelection = true; } if (showCurrentSelection) { //if (subsetSelected) { for (int dataOffset = startDataOffset; dataOffset <= endDataOffset; dataOffset++) { int frameOffset = m_Values[dataOffset].frameOffset; if (frameOffsetToSelectionIndex.ContainsKey(frameOffset)) { inSelectionRegion = true; break; } } } } return inSelectionRegion; } public void Draw(Rect rect, ProfileAnalysis analysis, List selectedFrameOffsets, float yMax, int offsetToDisplayMapping, int offsetToIndexMapping, string selectedMarkerName, int maxFrames = 0, ProfileAnalysis fullAnalysis = null) { Profiler.BeginSample("FrameTimeGraph.Draw"); // Must be outside repaint to make sure next controls work correctly (specifically marker name filter) int controlID = GUIUtility.GetControlID(FocusType.Keyboard); if (Event.current.type == EventType.Repaint) { m_LastRect = rect; // Control id would change during non repaint phase if tooltips displayed if we update this outside the repaint m_ControlID = controlID; } m_MaxFrames = maxFrames; if (m_Values == null) return; m_LastSelectedFrameOffsets = selectedFrameOffsets; int totalDataSize = m_Values.Count; if (totalDataSize <= 0) return; if (m_IsOrderedByFrameDuration != m_GlobalSettings.showOrderedByFrameDuration) { // Reorder if necessary SetData(m_Values); } // Get start and end selection span int currentSelectionFirstDataOffset; int currentSelectionLastDataOffset; int currentSelectionFirstFrameOffset; int currentSelectionLastFrameOffset; GetSelectedRange(selectedFrameOffsets, out currentSelectionFirstDataOffset, out currentSelectionLastDataOffset, out currentSelectionFirstFrameOffset, out currentSelectionLastFrameOffset); // Create mapping from offset to selection for faster selection detection Dictionary frameOffsetToSelectionIndex = new Dictionary(); for (int i = 0; i < selectedFrameOffsets.Count; i++) { int frameOffset = selectedFrameOffsets[i]; frameOffsetToSelectionIndex[frameOffset] = i; } Event current = Event.current; int selectedFirstOffset; int selectedLastOffset; int selectedCount; bool subsetSelected = false; if (HasDragRegion()) { selectedFirstOffset = m_DragFirstOffset; selectedLastOffset = m_DragLastOffset; if (selectedFirstOffset > m_Values.Count - 1) { // Selection off the end selectedFirstOffset = m_Values.Count; selectedLastOffset = m_Values.Count; selectedCount = 0; } else { selectedFirstOffset = ClampToRange(selectedFirstOffset, 0, m_Values.Count - 1); selectedLastOffset = ClampToRange(selectedLastOffset, 0, m_Values.Count - 1); selectedCount = 1 + (selectedLastOffset - selectedFirstOffset); subsetSelected = true; } } else { selectedFirstOffset = currentSelectionFirstDataOffset; selectedLastOffset = currentSelectionLastDataOffset; selectedCount = selectedFrameOffsets.Count; subsetSelected = (selectedCount > 0 && selectedCount != totalDataSize); } // Draw frames and selection float width = rect.width; float height = rect.height; bool showAxis = false; float xStart = 0f; float yStart = 0f; if (height > kYAxisDetailThreshold) { showAxis = true; float h = GUI.skin.label.lineHeight; xStart += kXAxisWidth; width -= kXAxisWidth; yStart += h; height -= h; } if (maxFrames > 0) { if (!m_Zoomed) width = width * totalDataSize / maxFrames; } // Start / End int startOffset = m_Zoomed ? m_ZoomStartOffset : 0; int endOffset = m_Zoomed ? m_ZoomEndOffset : totalDataSize - 1; // Get try index values int startIndex = offsetToDisplayMapping + startOffset; int endIndex = offsetToDisplayMapping + endOffset; int selectedFirstIndex = offsetToDisplayMapping + selectedFirstOffset; int selectedLastIndex = offsetToDisplayMapping + selectedLastOffset; string detailsString = ""; if (!showAxis) { string frameRangeString; if (startIndex == endIndex) frameRangeString = string.Format("Total Range {0}", startIndex); else frameRangeString = string.Format("Total Range {0} - {1} [{2}]", startIndex, endIndex, 1 + (endIndex - startIndex)); // Selection range string selectedTooltip = ""; if (subsetSelected) { if (selectedFirstIndex == selectedLastIndex) selectedTooltip = string.Format("\nSelected {0}\n", selectedFirstIndex); else selectedTooltip = string.Format("\nSelected {0} - {1} [{2}]", selectedFirstIndex, selectedLastIndex, selectedCount); } detailsString = string.Format("\n\n{0}{1}", frameRangeString, selectedTooltip); } float yRange = GetYAxisRange(yMax); bool lastEnabled = GUI.enabled; bool enabled = IsEnabled(); GUI.enabled = enabled; if (m_2D.DrawStart(rect, Draw2D.Origin.BottomLeft)) { float totalMs; if (HasDragRegion()) { totalMs = GetTotalSelectionTime(selectedFirstOffset, selectedLastOffset); } else { totalMs = GetTotalSelectionTime(selectedFrameOffsets); } string timeForSelectedFrames = ToDisplayUnits(totalMs, true, 0); string timeForSelectedFramesClamped = ToDisplayUnits(totalMs, true, 1); string selectionAreaString = string.Format("\n\nTotal time for {0} selected frames\n{1} ({2})", selectedCount, timeForSelectedFrames, timeForSelectedFramesClamped); Color selectedControl = GUI.skin.settings.selectionColor; if (IsGraphActive()) { m_2D.DrawBox(xStart, yStart, width, height, selectedControl); } //xStart -= 1f; yStart += 1f; width -= 1f; height -= 1f; m_2D.DrawFilledBox(xStart, yStart, width, height, m_ColorBarBackground); RegenerateBars(xStart, yStart, width, height, yRange); foreach (BarData bar in m_Bars) { bool inSelectionRegion = InSelectedRegion(bar.startDataOffset, bar.endDataOffset, selectedFirstOffset, selectedLastOffset, frameOffsetToSelectionIndex, subsetSelected); if (inSelectionRegion) { m_2D.DrawFilledBox(bar.x, bar.y, bar.w, height, m_ColorBarBackgroundSelected); } } if (m_GlobalSettings.showFrameLines) { ShowFrameLines(xStart, yStart, yRange, width, height); } ProfileAnalysis analysisData = analysis; bool full = false; if (fullAnalysis != null) { analysisData = fullAnalysis; full = true; } MarkerData selectedMarker = (m_GlobalSettings.showSelectedMarker && analysisData != null) ? analysisData.GetMarkerByName(selectedMarkerName) : null; foreach (BarData bar in m_Bars) { bool inSelectionRegion = InSelectedRegion(bar.startDataOffset, bar.endDataOffset, selectedFirstOffset, selectedLastOffset, frameOffsetToSelectionIndex, subsetSelected); if (inSelectionRegion) { m_2D.DrawFilledBox(bar.x, bar.y, bar.w, bar.h, m_ColorBarSelected); } else { m_2D.DrawFilledBox(bar.x, bar.y, bar.w, bar.h, m_ColorBar); } // Show where its been clamped if (bar.yMax > yRange) { m_2D.DrawFilledBox(bar.x, bar.y + height, 1, kOverrunHeight, m_ColorBarOutOfRange); } if (analysisData != null && (full || !m_Dragging)) { // Analysis is just on the subset if (m_GlobalSettings.showThreads) { Profiler.BeginSample("FrameTimeGraph.ShowThreads"); ShowThreads(height, yRange, bar, full, analysisData.GetThreads(), subsetSelected, selectedFirstOffset, selectedLastOffset, offsetToIndexMapping, frameOffsetToSelectionIndex); Profiler.EndSample(); } if (m_GlobalSettings.showSelectedMarker) { // Analysis is just on the subset (unless we have full analysis data) ShowSelectedMarker(height, yRange, bar, full, selectedMarker, subsetSelected, selectedFirstOffset, selectedLastOffset, offsetToIndexMapping, frameOffsetToSelectionIndex); } } } m_2D.DrawEnd(); if (m_GlobalSettings.showFrameLines && m_GlobalSettings.showFrameLineText) { ShowFrameLinesText(rect, xStart, yStart, yRange, width, height, subsetSelected, selectedFirstOffset, selectedLastOffset); } foreach (BarData bar in m_Bars) { bool inSelectionRegion = InSelectedRegion(bar.startDataOffset, bar.endDataOffset, selectedFirstOffset, selectedLastOffset, frameOffsetToSelectionIndex, subsetSelected); // Draw tooltip for bar (or 1 pixel segment of bar) { int barStartIndex = offsetToDisplayMapping + m_Values[bar.startDataOffset].frameOffset; int barEndIndex = offsetToDisplayMapping + m_Values[bar.endDataOffset].frameOffset; string tooltip; if (barStartIndex == barEndIndex) tooltip = string.Format("Frame {0}\n{1}{2}", barStartIndex, ToDisplayUnits(bar.yMax, true), detailsString); else tooltip = string.Format("Frame {0}-{1}\n{2} max\n{3} min{4}", barStartIndex, barEndIndex, ToDisplayUnits(bar.yMax, true), ToDisplayUnits(bar.yMin, true), detailsString); if (inSelectionRegion) tooltip += selectionAreaString; GUI.Label(new Rect(rect.x + bar.x, rect.y + 5, bar.w, height), new GUIContent("", tooltip)); } } } GUI.enabled = lastEnabled; if (showAxis) { int zoomedSelectedFirstOffset = selectedFirstOffset; int zoomedSelectedLastOffset = selectedLastOffset; int zoomedSelectedCount = selectedCount; if (m_Zoomed) { if (selectedFirstOffset > endOffset || selectedLastOffset < startOffset) { zoomedSelectedCount = 0; } else { // Clamp selection range to zoom range zoomedSelectedFirstOffset = ClampToRange(selectedFirstOffset, startOffset, endOffset); zoomedSelectedLastOffset = ClampToRange(selectedLastOffset, startOffset, endOffset); if (HasDragRegion()) { zoomedSelectedCount = 1 + (zoomedSelectedLastOffset - zoomedSelectedFirstOffset); } else { zoomedSelectedCount = 0; foreach (var offset in selectedFrameOffsets) { if (offset >= startOffset && offset <= endOffset) { zoomedSelectedCount++; } } } } } ShowAxis(rect, xStart, width, startOffset, endOffset, zoomedSelectedFirstOffset, zoomedSelectedLastOffset, zoomedSelectedCount, selectedCount, yMax, totalDataSize, offsetToDisplayMapping); } GUI.enabled = enabled; if (rect.Contains(current.mousePosition) && current.type == EventType.ContextClick) { var analytic = ProfileAnalyzerAnalytics.BeginAnalytic(); ShowContextMenu(subsetSelected, selectedCount); current.Use(); ProfileAnalyzerAnalytics.SendUIVisibilityEvent(ProfileAnalyzerAnalytics.UIVisibility.FrameTimeContextMenu, analytic.GetDurationInSeconds(), true); } GUI.enabled = lastEnabled; Profiler.EndSample(); } void ShowThreads(float height, float yRange, BarData bar, bool full, List threads, bool subsetSelected, int selectedFirstOffset, int selectedLastOffset, int offsetToIndexMapping, Dictionary frameOffsetToSelectionIndex) { float max = float.MinValue; bool selected = false; for (int dataOffset = bar.startDataOffset; dataOffset <= bar.endDataOffset; dataOffset++) { int frameOffset = m_Values[dataOffset].frameOffset; if (!full && !frameOffsetToSelectionIndex.ContainsKey(frameOffset)) continue; float threadMs = 0f; foreach (var thread in threads) { int frameIndex = offsetToIndexMapping + frameOffset; var frame = thread.GetFrame(frameIndex); if (frame == null) continue; float ms = frame.Value.ms; if (ms > threadMs) threadMs = ms; } if (threadMs > max) max = threadMs; if (m_Dragging) { if (frameOffset >= selectedFirstOffset && frameOffset <= selectedLastOffset) selected = true; } else if (subsetSelected) { if (frameOffsetToSelectionIndex.ContainsKey(frameOffset)) selected = true; } } if (full || selected) { // Clamp to frame time (these values can be time summed over multiple threads) if (max > bar.yMax) max = bar.yMax; float maxClamped = Math.Min(max, yRange); float h = height * maxClamped / yRange; m_2D.DrawFilledBox(bar.x, bar.y, bar.w, h, selected ? m_ColorBarThreadsSelected : m_ColorBarThreads); // Show where its been clamped if (max > yRange) { m_2D.DrawFilledBox(bar.x, bar.y + height, bar.w, kOverrunHeight, m_ColorBarThreadsOutOfRange); } } } void ShowSelectedMarker(float height, float yRange, BarData bar, bool full, MarkerData selectedMarker, bool subsetSelected, int selectedFirstOffset, int selectedLastOffset, int offsetToIndexMapping, Dictionary frameOffsetToSelectionIndex) { float max = 0f; bool selected = false; if (selectedMarker != null) { for (int dataOffset = bar.startDataOffset; dataOffset <= bar.endDataOffset; dataOffset++) { int frameOffset = m_Values[dataOffset].frameOffset; if (!full && !frameOffsetToSelectionIndex.ContainsKey(frameOffset)) continue; float ms = selectedMarker.GetFrameMs(offsetToIndexMapping + frameOffset); if (ms > max) max = ms; if (m_Dragging) { if (frameOffset >= selectedFirstOffset && frameOffset <= selectedLastOffset) selected = true; } else if (subsetSelected) { if (frameOffsetToSelectionIndex.ContainsKey(frameOffset)) selected = true; } } } if (full || selected) { // Clamp to frame time (these values can be tiem summed over multiple threads) if (max > bar.yMax) max = bar.yMax; float maxClamped = Math.Min(max, yRange); float h = height * maxClamped / yRange; m_2D.DrawFilledBox(bar.x, bar.y, bar.w, h, selected ? m_ColorBarMarkerSelected : m_ColorBarMarker); if (max > 0f) { // we start the bar lower so that very small markers still show up. m_2D.DrawFilledBox(bar.x, bar.y - kOverrunHeight, bar.w, kOverrunHeight, m_ColorBarMarkerOutOfRange); } // Show where its been clamped if (max > yRange) { m_2D.DrawFilledBox(bar.x, bar.y + height, bar.w, kOverrunHeight, m_ColorBarMarkerOutOfRange); } } } void ShowFrameLinesText(Rect rect, float xStart, float yStart, float yRange, float width, float height, bool subsetSelected, int selectedFirstOffset, int selectedLastOffset) { int totalDataSize = m_Values.Count; float y = yStart; float msSegment = 1000f / 60f; int lines = (int)(yRange / msSegment); int step = 1; for (int line = 1; line <= lines; line += step, step *= 2) { float ms = line * msSegment; float h = height * ms / yRange; int edgePad = 3; if (h >= (height / 4) && h < (height - GUI.skin.label.lineHeight)) { GUIContent content = new GUIContent(ToDisplayUnits((float)Math.Floor(ms), true, 0)); Vector2 size = EditorStyles.miniTextField.CalcSize(content); bool left = true; if (subsetSelected) { float x = GetXForDataOffset(selectedFirstOffset, (int)width, totalDataSize); float x2 = GetXForDataOffset(selectedLastOffset + 1, (int)width, totalDataSize); // text would overlap selection so move it if that prevents overlap if (left) { if (x < (size.x + edgePad) && x2 < (width - (size.x + edgePad))) left = false; } else { if (x > (size.x + edgePad) && x2 > (width - (size.x + edgePad))) left = true; } } Rect r; if (left) r = new Rect(rect.x + (xStart + edgePad), (rect.y - y) + (height - h), size.x, EditorStyles.miniTextField.lineHeight); else r = new Rect(rect.x + (xStart + width) - (size.x + edgePad), (rect.y - y) + (height - h), size.x, EditorStyles.miniTextField.lineHeight); GUI.Label(r, content, EditorStyles.miniTextField); } } } void ShowSelectionMenuItem(bool subsetSelected, GenericMenu menu, GUIContent style, bool state, GenericMenu.MenuFunction func) { if (subsetSelected) menu.AddItem(style, state, func); else menu.AddDisabledItem(style); } void ShowContextMenu(bool subsetSelected, int selectionCount) { GenericMenu menu = new GenericMenu(); bool showselectionOptions = subsetSelected || ((m_PairedWithFrameTimeGraph != null) && m_PairedWithFrameTimeGraph.HasSubsetSelected()); ShowSelectionMenuItem(showselectionOptions || selectionCount == 0, menu, Styles.menuItemSelectAll, false, () => SelectAll()); ShowSelectionMenuItem(showselectionOptions || selectionCount == m_Values.Count, menu, Styles.menuItemClearSelection, false, () => ClearSelection()); menu.AddItem(Styles.menuItemInvertSelection, false, () => InvertSelection()); menu.AddItem(Styles.menuItemSelectMin, false, () => SelectMin()); menu.AddItem(Styles.menuItemSelectMax, false, () => SelectMax()); menu.AddItem(Styles.menuItemSelectMedian, false, () => SelectMedian()); menu.AddSeparator(""); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectPrevious, false, () => SelectPrevious(1)); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectNext, false, () => SelectNext(1)); menu.AddSeparator(""); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectGrow, false, () => SelectGrow(1)); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectShrink, false, () => SelectShrink(1)); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectGrowLeft, false, () => SelectGrowLeft(1)); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectGrowRight, false, () => SelectGrowRight(1)); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectShrinkLeft, false, () => SelectShrinkLeft(1)); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectShrinkRight, false, () => SelectShrinkRight(1)); menu.AddSeparator(""); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectGrowFast, false, () => SelectGrow(10)); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectShrinkFast, false, () => SelectShrink(10)); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectGrowLeftFast, false, () => SelectGrowLeft(10)); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectGrowRightFast, false, () => SelectGrowRight(10)); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectShrinkLeftFast, false, () => SelectShrinkLeft(10)); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemSelectShrinkRightFast, false, () => SelectShrinkRight(10)); menu.AddSeparator(""); ShowSelectionMenuItem(showselectionOptions, menu, Styles.menuItemZoomSelection, false, () => ZoomSelection()); ShowSelectionMenuItem(m_Zoomed, menu, Styles.menuItemZoomAll, false, () => ZoomAll()); menu.AddSeparator(""); menu.AddItem(Styles.menuItemShowSelectedMarker, m_GlobalSettings.showSelectedMarker, () => ToggleShowSelectedMarker()); menu.AddItem(Styles.menuItemShowThreads, m_GlobalSettings.showThreads, () => ToggleShowThreads()); menu.AddItem(Styles.menuItemShowFrameLines, m_GlobalSettings.showFrameLines, () => ToggleShowFrameLines()); //menu.AddItem(Styles.menuItemShowFrameLineText, m_GlobalSettings.showFrameLineText, () => ToggleShowFrameLinesText()); menu.AddSeparator(""); menu.AddItem(Styles.menuItemShowOrderedByFrameDuration, m_GlobalSettings.showOrderedByFrameDuration, () => ToggleShowOrderedByFrameDuration()); menu.ShowAsContext(); } string GetYMaxText(float value) { return ToDisplayUnits(value, true, 0); } void DrawYAxisRangeSelector(Rect rect, float yMax) { string yMaxText = GetYMaxText(yMax); List yAxisOptions = new List(); var graphScaleUnits = ToDisplayUnits(1000f / 60f, true, 0); yAxisOptions.Add(new GUIContent(graphScaleUnits, string.Format("Graph Scale : {0} is equivalent to 60Hz or 60FPS.", graphScaleUnits))); graphScaleUnits = ToDisplayUnits(1000f / 30f, true, 0); yAxisOptions.Add(new GUIContent(graphScaleUnits, string.Format("Graph Scale : {0} is equivalent to 30Hz or 30FPS.", graphScaleUnits))); graphScaleUnits = ToDisplayUnits(1000f / 15f, true, 0); yAxisOptions.Add(new GUIContent(graphScaleUnits, string.Format("Graph Scale : {0} is equivalent to 15Hz or 15FPS.", graphScaleUnits))); yAxisOptions.Add(new GUIContent(yMaxText, "Graph Scale : Max frame time from data")); float width = 0; foreach (var content in yAxisOptions) { Vector2 size = EditorStyles.popup.CalcSize(content); if (size.x > width) width = size.x; } // Use smaller width if text is shorter int margin = 2; width = Math.Min(width + margin, rect.width); // Shift right to right align rect.x += (rect.width - width); rect.x -= margin; rect.width = width; s_YAxisMode = (AxisMode)EditorGUI.Popup(rect, (int)s_YAxisMode, yAxisOptions.ToArray()); } void ShowAxis(Rect rect, float xStart, float width, int startOffset, int endOffset, int selectedFirstOffset, int selectedLastOffset, int selectedCount, int totalSelectedCount, float yMax, int totalDataSize, int offsetToDisplayMapping) { GUIStyle leftAlignStyle = new GUIStyle(GUI.skin.label); leftAlignStyle.padding = new RectOffset(leftAlignStyle.padding.left, leftAlignStyle.padding.right, 0, 0); leftAlignStyle.alignment = TextAnchor.MiddleLeft; GUIStyle rightAlignStyle = new GUIStyle(GUI.skin.label); rightAlignStyle.padding = new RectOffset(rightAlignStyle.padding.left, rightAlignStyle.padding.right, 0, 0); rightAlignStyle.alignment = TextAnchor.MiddleRight; // y axis float h = GUI.skin.label.lineHeight; float y = rect.y + ((rect.height - 1) - h); DrawYAxisRangeSelector(new Rect(rect.x, rect.y, kXAxisWidth, h), yMax); string yMinText = ToDisplayUnits(0, true); GUI.Label(new Rect(rect.x, y - h, kXAxisWidth, h), yMinText, rightAlignStyle); // x axis rect.x += xStart; leftAlignStyle.padding = new RectOffset(0, 0, leftAlignStyle.padding.top, leftAlignStyle.padding.bottom); rightAlignStyle.padding = new RectOffset(0, 0, rightAlignStyle.padding.top, rightAlignStyle.padding.bottom); int startIndex = offsetToDisplayMapping + startOffset; string startIndexText = string.Format("{0}", startIndex); GUIContent startIndexContent = new GUIContent(startIndexText); Vector2 startIndexSize = GUI.skin.label.CalcSize(startIndexContent); bool drawStart = !m_GlobalSettings.showOrderedByFrameDuration; int endIndex = offsetToDisplayMapping + endOffset; string endIndexText = string.Format("{0}", endIndex); GUIContent endIndexContent = new GUIContent(endIndexText); Vector2 endIndexSize = GUI.skin.label.CalcSize(endIndexContent); bool drawEnd = !m_GlobalSettings.showOrderedByFrameDuration; // Show selection frame values (if space for them) if (totalSelectedCount > 0) { if (selectedCount == 0) { // If we have no selection then adjust 'selection start/end to span whole view so the count display is centred) selectedFirstOffset = startOffset; selectedLastOffset = endOffset; } int selectedFirstX = GetXForDataOffset(selectedFirstOffset, (int)width, totalDataSize); int selectedLastX = GetXForDataOffset(selectedLastOffset + 1, (int)width, totalDataSize); // last + 1 so right hand side of the bbar int selectedRangeWidth = 1 + (selectedLastX - selectedFirstX); int selectedFirstIndex = offsetToDisplayMapping + selectedFirstOffset; int selectedLastIndex = offsetToDisplayMapping + selectedLastOffset; string selectionCountText; if (totalSelectedCount != selectedCount) selectionCountText = string.Format("[{0} of {1}]", selectedCount, totalSelectedCount); else selectionCountText = string.Format("[{0}]", selectedCount); string selectionRangeText; if (selectedCount > 1) { if (m_GlobalSettings.showOrderedByFrameDuration) selectionRangeText = selectionCountText; else selectionRangeText = string.Format("{0} {1} {2}", selectedFirstIndex, selectionCountText, selectedLastIndex); } else selectionRangeText = string.Format("{0} {1}", selectedFirstIndex, selectionCountText); string tooltip = string.Format("{0} frames in selection", selectedCount); if (totalSelectedCount != selectedCount) { tooltip = string.Format("{0} frames in zoomed selection\n{1} frames in overall selection", selectedCount, totalSelectedCount); } GUIContent selectionRangeTextContent = new GUIContent(selectionRangeText, tooltip); Vector2 selectionRangeTextSize = GUI.skin.label.CalcSize(selectionRangeTextContent); if ((selectedRangeWidth > selectionRangeTextSize.x && selectedCount > 1) || selectedCount == 0) { // Selection width is larger than the text so we can split the text string selectedFirstIndexText = string.Format("{0}", selectedFirstIndex); GUIContent selectedFirstIndexContent = new GUIContent(selectedFirstIndexText); Vector2 selectedFirstIndexSize = GUI.skin.label.CalcSize(selectedFirstIndexContent); if (m_GlobalSettings.showOrderedByFrameDuration) selectedFirstIndexSize.x = 0; string selectedLastIndexText = string.Format("{0}", selectedLastIndex); GUIContent selectedLastIndexContent = new GUIContent(selectedLastIndexText); Vector2 selectedLastIndexSize = GUI.skin.label.CalcSize(selectedLastIndexContent); if (m_GlobalSettings.showOrderedByFrameDuration) selectedLastIndexSize.x = 0; GUIContent selectedCountContent = new GUIContent(selectionCountText, tooltip); Vector2 selectedCountSize = GUI.skin.label.CalcSize(selectedCountContent); Rect rFirst = new Rect(rect.x + selectedFirstX, y, selectedFirstIndexSize.x, selectedFirstIndexSize.y); GUI.Label(rFirst, selectedFirstIndexContent); Rect rLast = new Rect(rect.x + selectedLastX - selectedLastIndexSize.x, y, selectedLastIndexSize.x, selectedLastIndexSize.y); GUI.Label(rLast, selectedLastIndexContent); float mid = selectedFirstX + ((selectedLastX - selectedFirstX) / 2); Rect rCount = new Rect(rect.x + mid - (selectedCountSize.x / 2), y, selectedCountSize.x, selectedCountSize.y); GUI.Label(rCount, selectedCountContent); if (selectedFirstX < startIndexSize.x) { // would overlap with start text drawStart = false; } if (selectedLastX > ((width - 1) - endIndexSize.x)) { // would overlap with end text drawEnd = false; } } else { int mid = (selectedFirstX + (selectedRangeWidth / 2)); int selectionTextX = mid - (int)(selectionRangeTextSize.x / 2); selectionTextX = ClampToRange(selectionTextX, 0, (int)((width - 1) - selectionRangeTextSize.x)); Rect rangeRect = new Rect(rect.x + selectionTextX, y, selectionRangeTextSize.x, selectionRangeTextSize.y); GUI.Label(rangeRect, selectionRangeTextContent); if (selectionTextX < startIndexSize.x) { // would overlap with start text drawStart = false; } if ((selectionTextX + selectionRangeTextSize.x) > ((width - 1) - endIndexSize.x)) { // would overlap with end text drawEnd = false; } } } // Show start and end values if (drawStart) { Rect leftRect = new Rect(rect.x, y, startIndexSize.x, startIndexSize.y); GUI.Label(leftRect, startIndexContent, leftAlignStyle); } if (drawEnd) { Rect rightRect = new Rect(rect.x + ((width - 1) - endIndexSize.x), y, endIndexSize.x, endIndexSize.y); GUI.Label(rightRect, endIndexContent, rightAlignStyle); } } void ClearSelection(bool effectPaired = true) { int dataLength = m_Values.Count; bool singleControlAction = true; // As we need the frame range to be unique to each data set CallSetRange(-1, -1, 0, singleControlAction, FrameTimeGraph.State.DragComplete); // Disable zoom m_Zoomed = false; if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.ClearSelection(false); } void SelectAll(bool effectPaired = true) { int dataLength = m_Values.Count; bool singleControlAction = true; // As we need the frame range to be unique to each data set CallSetRange(0, dataLength - 1, 0, singleControlAction, FrameTimeGraph.State.DragComplete); // Disable zoom m_Zoomed = false; if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.SelectAll(false); } void InvertSelection(bool effectPaired = true) { int dataLength = m_Values.Count; Dictionary frameOffsetToSelectionIndex = new Dictionary(); for (int i = 0; i < m_CurrentSelection.Count; i++) { int frameOffset = m_CurrentSelection[i]; frameOffsetToSelectionIndex[frameOffset] = i; } m_CurrentSelection.Clear(); for (int frameIndex = 0; frameIndex < dataLength; frameIndex++) { if (!frameOffsetToSelectionIndex.ContainsKey(frameIndex)) m_CurrentSelection.Add(frameIndex); } m_SetRange(m_CurrentSelection, 1, FrameTimeGraph.State.DragComplete); // Disable zoom m_Zoomed = false; if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.InvertSelection(false); } void ZoomSelection(bool effectPaired = true) { m_Zoomed = true; m_ZoomStartOffset = m_CurrentSelectionFirstDataOffset; m_ZoomEndOffset = m_CurrentSelectionLastDataOffset; if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.ZoomSelection(false); } void ZoomAll(bool effectPaired = true) { m_Zoomed = false; int frames = m_Values.Count; m_ZoomStartOffset = 0; m_ZoomEndOffset = frames - 1; if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.ZoomAll(false); } void SelectMin(bool effectPaired = true) { int dataLength = m_Values.Count; if (dataLength <= 0) return; int minDataOffset = 0; float msMin = m_Values[0].ms; for (int dataOffset = 0; dataOffset < dataLength; dataOffset++) { float ms = m_Values[dataOffset].ms; if (ms < msMin) { msMin = ms; minDataOffset = dataOffset; } } bool singleControlAction = true; CallSetRange(minDataOffset, minDataOffset, 1, singleControlAction, State.DragComplete); // act like single click, so we select frame //CallSetRange(minDataOffset, minDataOffset, 2, singleControlAction, State.DragComplete); // act like double click, so we jump to the frame m_Zoomed = false; if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.SelectMin(false); } void SelectMax(bool effectPaired = true) { int dataLength = m_Values.Count; if (dataLength <= 0) return; int maxDataOffset = 0; float msMax = m_Values[0].ms; for (int dataOffset = 0; dataOffset < dataLength; dataOffset++) { float ms = m_Values[dataOffset].ms; if (ms > msMax) { msMax = ms; maxDataOffset = dataOffset; } } bool singleControlAction = true; CallSetRange(maxDataOffset, maxDataOffset, 1, singleControlAction, State.DragComplete); // act like single click, so we select frame //CallSetRange(maxDataOffset, maxDataOffset, 2, singleControlAction, State.DragComplete); // act like double click, so we jump to the frame m_Zoomed = false; if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.SelectMax(false); } float GetPercentageOffset(List data, float percent, out int outputFrameOffset) { int index = (int)((data.Count - 1) * percent / 100); outputFrameOffset = data[index].frameOffset; // 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 data[index].ms; } void SelectMedian(bool effectPaired = true) { int dataLength = m_Values.Count; if (dataLength <= 0) return; List sortedValues = new List(); foreach (var value in m_Values) { Data data = new Data(value.ms, value.frameOffset); sortedValues.Add(data); } // If ms value is the same then order by frame offset sortedValues.Sort((a, b) => { int compare = a.ms.CompareTo(b.ms); return compare == 0 ? a.frameOffset.CompareTo(b.frameOffset) : compare; }); int medianFrameOffset = 0; GetPercentageOffset(sortedValues, 50, out medianFrameOffset); int medianDataOffset = m_FrameOffsetToDataOffsetMapping[medianFrameOffset]; bool singleControlAction = true; CallSetRange(medianDataOffset, medianDataOffset, 1, singleControlAction, State.DragComplete); // act like single click, so we select frame //CallSetRange(medianDataOffset, medianDataOffset, 2, singleControlAction, State.DragComplete); // act like double click, so we jump to the frame m_Zoomed = false; if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.SelectMedian(false); } public void SelectPrevious(int step, bool effectPaired = true) { int clicks = 1; bool singleClickAction = true; MoveSelectedRange(-step, clicks, singleClickAction, State.DragComplete); if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.SelectPrevious(step, false); } public void SelectNext(int step, bool effectPaired = true) { int clicks = 1; bool singleClickAction = true; MoveSelectedRange(step, clicks, singleClickAction, State.DragComplete); if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.SelectNext(step, false); } public void SelectGrow(int step, bool effectPaired = true) { int clicks = 1; bool singleClickAction = true; ResizeSelectedRange(-step, step, clicks, singleClickAction, State.DragComplete); if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.SelectGrow(step, false); } public void SelectGrowLeft(int step, bool effectPaired = true) { int clicks = 1; bool singleClickAction = true; ResizeSelectedRange(-step, 0, clicks, singleClickAction, State.DragComplete); if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.SelectGrowLeft(step, false); } public void SelectGrowRight(int step, bool effectPaired = true) { int clicks = 1; bool singleClickAction = true; ResizeSelectedRange(0, step, clicks, singleClickAction, State.DragComplete); if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.SelectGrowRight(step, false); } public void SelectShrink(int step, bool effectPaired = true) { int clicks = 1; bool singleClickAction = true; ResizeSelectedRange(step, -step, clicks, singleClickAction, State.DragComplete); if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.SelectShrink(step, false); } public void SelectShrinkLeft(int step, bool effectPaired = true) { int clicks = 1; bool singleClickAction = true; ResizeSelectedRange(step, 0, clicks, singleClickAction, State.DragComplete); if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.SelectShrinkLeft(step, false); } public void SelectShrinkRight(int step, bool effectPaired = true) { int clicks = 1; bool singleClickAction = true; ResizeSelectedRange(0, -step, clicks, singleClickAction, State.DragComplete); if (m_PairedWithFrameTimeGraph != null && effectPaired) m_PairedWithFrameTimeGraph.SelectShrinkRight(step, false); } public void ToggleShowThreads() { m_GlobalSettings.showThreads ^= true; } public void ToggleShowSelectedMarker() { m_GlobalSettings.showSelectedMarker ^= true; } public void ToggleShowFrameLines() { m_GlobalSettings.showFrameLines ^= true; } public void ToggleShowFrameLinesText() { m_GlobalSettings.showFrameLineText ^= true; } public void ToggleShowOrderedByFrameDuration() { m_GlobalSettings.showOrderedByFrameDuration ^= true; SetData(m_Values); // Update order if (m_PairedWithFrameTimeGraph != null) { m_PairedWithFrameTimeGraph.SetData(m_PairedWithFrameTimeGraph.m_Values); // Update order } } internal struct SelectedRangeState { public int currentSelectionFirstDataOffset; public int currentSelectionLastDataOffset; public List lastSelectedFrameOffsets; } void MoveSelectedRange(int offset, int clickCount, bool singleControlAction, State inputStatus) { MoveSelectedRange(offset, clickCount, singleControlAction, inputStatus, new SelectedRangeState() { currentSelectionFirstDataOffset = m_CurrentSelectionFirstDataOffset, currentSelectionLastDataOffset = m_CurrentSelectionLastDataOffset, lastSelectedFrameOffsets = m_LastSelectedFrameOffsets, }); } internal void MoveSelectedRange(int offset, int clickCount, bool singleControlAction, State inputStatus, SelectedRangeState selectedRange) { var currentSelectionFirstDataOffset = selectedRange.currentSelectionFirstDataOffset; var currentSelectionLastDataOffset = selectedRange.currentSelectionLastDataOffset; var lastSelectedFrameOffsets = selectedRange.lastSelectedFrameOffsets; if (offset < 0) { // Clamp offset to graph lower bound. if (currentSelectionFirstDataOffset + offset < 0) { offset = -currentSelectionFirstDataOffset; } } else { // Clamp offset to graph upper bound. if (currentSelectionLastDataOffset + offset >= m_Values.Count) { offset = (m_Values.Count - 1) - currentSelectionLastDataOffset; } } // Offset selection. List selected = new List(); foreach (int selectedFrameOffset in lastSelectedFrameOffsets) { var selectedDataOffset = m_FrameOffsetToDataOffsetMapping[selectedFrameOffset]; var newDataOffset = selectedDataOffset + offset; if (newDataOffset >= 0 && newDataOffset < m_Values.Count) { var newFrameOffset = m_Values[newDataOffset].frameOffset; if (!selected.Contains(newFrameOffset)) { selected.Add(newFrameOffset); } } } // Sort selection in frame index order. selected.Sort(); m_SetRange(selected, clickCount, inputStatus); } void ResizeSelectedRange(int leftOffset, int rightOffset, int clickCount, bool singleControlAction, State inputState) { ResizeSelectedRange(leftOffset, rightOffset, clickCount, singleControlAction, inputState, new SelectedRangeState() { currentSelectionFirstDataOffset = m_CurrentSelectionFirstDataOffset, currentSelectionLastDataOffset = m_CurrentSelectionLastDataOffset, lastSelectedFrameOffsets = m_LastSelectedFrameOffsets, }); } internal void ResizeSelectedRange(int leftOffset, int rightOffset, int clickCount, bool singleControlAction, State inputState, SelectedRangeState selectedRange) { const int k_InvalidDataOffset = -1; var currentSelectionFirstDataOffset = selectedRange.currentSelectionFirstDataOffset; var currentSelectionLastDataOffset = selectedRange.currentSelectionLastDataOffset; var lastSelectedFrameOffsets = selectedRange.lastSelectedFrameOffsets; // Clamp left offset to lower graph bound. bool isGrowingLeft = leftOffset < 0; if (isGrowingLeft && currentSelectionFirstDataOffset + leftOffset < 0) { leftOffset = -currentSelectionFirstDataOffset; } // Clamp right offset to upper graph bound. bool isGrowingRight = rightOffset > 0; if (isGrowingRight && currentSelectionLastDataOffset + rightOffset > (m_Values.Count - 1)) { rightOffset = (m_Values.Count - 1) - currentSelectionLastDataOffset; } int selectionStartDataOffset = k_InvalidDataOffset; List selected = new List(lastSelectedFrameOffsets); for (int dataOffset = 0; dataOffset < m_Values.Count; ++dataOffset) { var frameOffset = m_Values[dataOffset].frameOffset; if (selectionStartDataOffset == k_InvalidDataOffset) { // Find selection start. if (lastSelectedFrameOffsets.Contains(frameOffset)) { selectionStartDataOffset = dataOffset; } } else { // Find selection end. bool isSelected = lastSelectedFrameOffsets.Contains(frameOffset); if (!isSelected || dataOffset == (m_Values.Count - 1)) { int selectionEndDataOffset; // If we reached the last index and it is selected, this index is the selection end. Otherwise, the previous index is the last selected. bool isLastIndex = dataOffset == (m_Values.Count - 1); if (isLastIndex && isSelected) { selectionEndDataOffset = dataOffset; } else { selectionEndDataOffset = dataOffset - 1; } int newSelectionStartDataOffset = Mathf.Clamp(selectionStartDataOffset + leftOffset, 0, m_Values.Count - 1); int newSelectionEndDataOffset = Mathf.Clamp(selectionEndDataOffset + rightOffset, 0, m_Values.Count - 1); // Enforce a minimum selection width. if (newSelectionEndDataOffset < newSelectionStartDataOffset) { var maximumOffset = (selectionEndDataOffset - selectionStartDataOffset) * 0.5f; var adjustedLeftOffset = Mathf.CeilToInt(maximumOffset); newSelectionStartDataOffset = Mathf.Clamp(selectionStartDataOffset + adjustedLeftOffset, 0, m_Values.Count - 1); var adjustedRightOffset = -Mathf.FloorToInt(maximumOffset); newSelectionEndDataOffset = Mathf.Clamp(selectionEndDataOffset + adjustedRightOffset, 0, m_Values.Count - 1); } if (selectionStartDataOffset != newSelectionStartDataOffset) { // Resize from left edge. int startDataOffset = Mathf.Min(selectionStartDataOffset, newSelectionStartDataOffset); int endDataOffset = Mathf.Max(selectionStartDataOffset, newSelectionStartDataOffset); MoveSelectionEdge(startDataOffset, endDataOffset, isGrowingLeft, ref selected); } if (selectionEndDataOffset != newSelectionEndDataOffset) { // Resize from right edge (iterate backwards). int startDataOffset = Mathf.Max(selectionEndDataOffset, newSelectionEndDataOffset); int endDataOffset = Mathf.Min(selectionEndDataOffset, newSelectionEndDataOffset); MoveSelectionEdge(startDataOffset, endDataOffset, isGrowingRight, ref selected); } // Reset to find next selection. selectionStartDataOffset = k_InvalidDataOffset; } } } // Sort selection in frame index order. selected.Sort(); m_SetRange(selected, clickCount, inputState); } void MoveSelectionEdge(int startDataOffset, int endDataOffset, bool isGrowingFromEdge, ref List selection) { var direction = (startDataOffset >= endDataOffset) ? -1 : 1; for (int dataOffset = startDataOffset; dataOffset != endDataOffset; dataOffset += direction) { var frameOffset = m_Values[dataOffset].frameOffset; var indexInSelection = selection.IndexOf(frameOffset); if (isGrowingFromEdge) { if (indexInSelection == -1) { selection.Add(frameOffset); } } else { if (indexInSelection != -1) { selection.RemoveAt(indexInSelection); } } } } } }