diff --git a/Ryujinx.Profiler/InternalProfile.cs b/Ryujinx.Profiler/InternalProfile.cs index eb2a4a2ce3..2b263affb7 100644 --- a/Ryujinx.Profiler/InternalProfile.cs +++ b/Ryujinx.Profiler/InternalProfile.cs @@ -9,6 +9,8 @@ namespace Ryujinx.Profiler { public class InternalProfile { + public long CurrentTime => SW.ElapsedTicks; + private Stopwatch SW; internal ConcurrentDictionary Timers; diff --git a/Ryujinx.Profiler/Profile.cs b/Ryujinx.Profiler/Profile.cs index 8c3538e5e9..7fd7a6bd77 100644 --- a/Ryujinx.Profiler/Profile.cs +++ b/Ryujinx.Profiler/Profile.cs @@ -6,10 +6,12 @@ namespace Ryujinx.Profiler { public static class Profile { - // Static private static InternalProfile _profileInstance; private static ProfilerSettings _settings; + public static float UpdateRate => _settings.UpdateRate; + public static long HistoryLength => _settings.History; + public static bool ProfilingEnabled() { if (!_settings.Enabled) @@ -89,9 +91,13 @@ namespace Ryujinx.Profiler return _profileInstance.GetProfilingData(); } - public static float GetUpdateRate() + + + public static long GetCurrentTime() { - return _settings.UpdateRate; + if (!ProfilingEnabled()) + return 0; + return _profileInstance.CurrentTime; } } } diff --git a/Ryujinx.Profiler/TimingInfo.cs b/Ryujinx.Profiler/TimingInfo.cs index 96cba5c7e9..4ed7535374 100644 --- a/Ryujinx.Profiler/TimingInfo.cs +++ b/Ryujinx.Profiler/TimingInfo.cs @@ -22,50 +22,70 @@ namespace Ryujinx.Profiler // Work out average public long AverageTime => (Count == 0) ? -1 : TotalTime / Count; + // Intentionally not locked as it's only a get count + public bool IsActive => _timestamps.Count > 0; + + public long BeginTime + { + get + { + lock (_timestampLock) + { + if (_depth > 0) + { + return _currentTimestamp.BeginTime; + } + + return -1; + } + } + } + // Timestamp collection - public List Timestamps; - private readonly object timestampLock = new object(); - private Timestamp currentTimestamp; + private List _timestamps; + private readonly object _timestampLock = new object(); + private readonly object _timestampListLock = new object(); + private Timestamp _currentTimestamp; // Depth of current timer, // each begin call increments and each end call decrements - private int depth; + private int _depth; public TimingInfo() { - Timestamps = new List(); - depth = 0; + _timestamps = new List(); + _depth = 0; } public void Begin(long beginTime) { - lock (timestampLock) + lock (_timestampLock) { // Finish current timestamp if already running - if (depth > 0) + if (_depth > 0) { EndUnsafe(beginTime); } BeginUnsafe(beginTime); - depth++; + _depth++; } } private void BeginUnsafe(long beginTime) { - currentTimestamp.BeginTime = beginTime; - currentTimestamp.EndTime = -1; + _currentTimestamp.BeginTime = beginTime; + _currentTimestamp.EndTime = -1; } public void End(long endTime) { - lock (timestampLock) + lock (_timestampLock) { - depth--; + _depth--; - if (depth < 0) + if (_depth < 0) { throw new Exception("Timing info end called without corresponding begin"); } @@ -73,7 +93,7 @@ namespace Ryujinx.Profiler EndUnsafe(endTime); // Still have others using this timing info so recreate start for them - if (depth > 0) + if (_depth > 0) { BeginUnsafe(endTime); } @@ -82,10 +102,13 @@ namespace Ryujinx.Profiler private void EndUnsafe(long endTime) { - currentTimestamp.EndTime = endTime; - Timestamps.Add(currentTimestamp); + _currentTimestamp.EndTime = endTime; + lock (_timestampListLock) + { + _timestamps.Add(_currentTimestamp); + } - var delta = currentTimestamp.EndTime - currentTimestamp.BeginTime; + var delta = _currentTimestamp.EndTime - _currentTimestamp.BeginTime; TotalTime += delta; Instant += delta; @@ -96,13 +119,13 @@ namespace Ryujinx.Profiler // Remove any timestamps before given timestamp to free memory public void Cleanup(long before) { - lock (timestampLock) + lock (_timestampListLock) { int toRemove = 0; - for (int i = 0; i < Timestamps.Count; i++) + for (int i = 0; i < _timestamps.Count; i++) { - if (Timestamps[i].EndTime < before) + if (_timestamps[i].EndTime < before) { toRemove++; } @@ -114,7 +137,19 @@ namespace Ryujinx.Profiler } if (toRemove > 0) - Timestamps.RemoveRange(0, toRemove); + { + _timestamps.RemoveRange(0, toRemove); + } + } + } + + public Timestamp[] GetAllTimestamps() + { + lock (_timestampListLock) + { + Timestamp[] returnTimestamps = new Timestamp[_timestamps.Count]; + _timestamps.CopyTo(returnTimestamps); + return returnTimestamps; } } } diff --git a/Ryujinx.Profiler/UI/ProfileWindow.cs b/Ryujinx.Profiler/UI/ProfileWindow.cs index 827bc5fc33..b2685cd160 100644 --- a/Ryujinx.Profiler/UI/ProfileWindow.cs +++ b/Ryujinx.Profiler/UI/ProfileWindow.cs @@ -51,7 +51,7 @@ namespace Ryujinx.Profiler.UI private const int TitleHeight = 24; private const int TitleFontHeight = 16; private const int LinePadding = 2; - private const int ColumnSpacing = 30; + private const int ColumnSpacing = 15; private const int FilterHeight = 24; // Sorting @@ -69,6 +69,7 @@ namespace Ryujinx.Profiler.UI // Profile data storage private List> _sortedProfileData; + private long _captureTime; // Input private bool _backspaceDown = false; @@ -78,7 +79,8 @@ namespace Ryujinx.Profiler.UI // Event management private double _updateTimer; private double _processEventTimer; - private bool _profileUpdated = false; + private bool _profileUpdated = false; + private readonly object _profileDataLock = new object(); public ProfileWindow() @@ -207,50 +209,47 @@ namespace Ryujinx.Profiler.UI // Get timing data if enough time has passed _updateTimer += e.Time; - if (!_paused && (_updateTimer > Profile.GetUpdateRate())) + if (!_paused && (_updateTimer > Profile.UpdateRate)) { _updateTimer = 0; _unsortedProfileData = Profile.GetProfilingData().ToList(); + _captureTime = Profile.GetCurrentTime(); _profileUpdated = true; } // Filtering if (_profileUpdated) { - if (_showInactive) + lock (_profileDataLock) { - _sortedProfileData = _unsortedProfileData; - } - else - { - _sortedProfileData = _unsortedProfileData.FindAll(kvp => kvp.Value.Instant > 0.001f); - } + _sortedProfileData = _showInactive ? _unsortedProfileData : _unsortedProfileData.FindAll(kvp => kvp.Value.IsActive); - if (_sortAction != null) - { - _sortedProfileData.Sort(_sortAction); - } - - if (_regexEnabled) - { - try + if (_sortAction != null) { - Regex filterRegex = new Regex(_filterText, RegexOptions.IgnoreCase); - if (_filterText != "") + _sortedProfileData.Sort(_sortAction); + } + + if (_regexEnabled) + { + try { - _sortedProfileData = _sortedProfileData.Where((pair => filterRegex.IsMatch(pair.Key.Search))).ToList(); + Regex filterRegex = new Regex(_filterText, RegexOptions.IgnoreCase); + if (_filterText != "") + { + _sortedProfileData = _sortedProfileData.Where((pair => filterRegex.IsMatch(pair.Key.Search))).ToList(); + } + } + catch (ArgumentException argException) + { + // Skip filtering for invalid regex } } - catch (ArgumentException argException) + else { - // Skip filtering for invalid regex + // Regular filtering + _sortedProfileData = _sortedProfileData.Where((pair => pair.Key.Search.ToLower().Contains(_filterText.ToLower()))).ToList(); } } - else - { - // Regular filtering - _sortedProfileData = _sortedProfileData.Where((pair => pair.Key.Search.ToLower().Contains(_filterText.ToLower()))).ToList(); - } _profileUpdated = false; _redrawPending = true; @@ -316,6 +315,8 @@ namespace Ryujinx.Profiler.UI float maxWidth = 0; float yOffset = _scrollPos - TitleHeight; float xOffset = 10; + float timingDataLeft; + float timingWidth; // Background lines to make reading easier #region Background Lines @@ -343,115 +344,130 @@ namespace Ryujinx.Profiler.UI GL.End(); _maxScroll = (LineHeight + LinePadding) * (_sortedProfileData.Count - 1); #endregion - - // Display category - #region Category - verticalIndex = 0; - foreach (var entry in _sortedProfileData) + + lock (_profileDataLock) { - float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); - width = _fontService.DrawText(entry.Key.Category, xOffset, y, LineHeight); - if (width > maxWidth) + // Display category + #region Category + verticalIndex = 0; + foreach (var entry in _sortedProfileData) { - maxWidth = width; + float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); + width = _fontService.DrawText(entry.Key.Category, xOffset, y, LineHeight); + + if (width > maxWidth) + { + maxWidth = width; + } } - } - GL.Disable(EnableCap.ScissorTest); + GL.Disable(EnableCap.ScissorTest); - width = _fontService.DrawText("Category", xOffset, Height - TitleFontHeight, TitleFontHeight); - if (width > maxWidth) - maxWidth = width; - - xOffset += maxWidth + ColumnSpacing; - #endregion - - // Display session group - #region Session Group - maxWidth = 0; - verticalIndex = 0; - - GL.Enable(EnableCap.ScissorTest); - foreach (var entry in _sortedProfileData) - { - float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); - width = _fontService.DrawText(entry.Key.SessionGroup, xOffset, y, LineHeight); + width = _fontService.DrawText("Category", xOffset, Height - TitleFontHeight, TitleFontHeight); if (width > maxWidth) - { maxWidth = width; + + xOffset += maxWidth + ColumnSpacing; + #endregion + + // Display session group + #region Session Group + maxWidth = 0; + verticalIndex = 0; + + GL.Enable(EnableCap.ScissorTest); + foreach (var entry in _sortedProfileData) + { + float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); + width = _fontService.DrawText(entry.Key.SessionGroup, xOffset, y, LineHeight); + + if (width > maxWidth) + { + maxWidth = width; + } } - } - GL.Disable(EnableCap.ScissorTest); + GL.Disable(EnableCap.ScissorTest); - width = _fontService.DrawText("Group", xOffset, Height - TitleFontHeight, TitleFontHeight); - if (width > maxWidth) - maxWidth = width; - - xOffset += maxWidth + ColumnSpacing; - #endregion - - // Display session item - - #region Session Item - maxWidth = 0; - verticalIndex = 0; - GL.Enable(EnableCap.ScissorTest); - foreach (var entry in _sortedProfileData) - { - float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); - width = _fontService.DrawText(entry.Key.SessionItem, xOffset, y, LineHeight); + width = _fontService.DrawText("Group", xOffset, Height - TitleFontHeight, TitleFontHeight); if (width > maxWidth) - { maxWidth = width; + + xOffset += maxWidth + ColumnSpacing; + #endregion + + // Display session item + #region Session Item + maxWidth = 0; + verticalIndex = 0; + GL.Enable(EnableCap.ScissorTest); + foreach (var entry in _sortedProfileData) + { + float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); + width = _fontService.DrawText(entry.Key.SessionItem, xOffset, y, LineHeight); + + if (width > maxWidth) + { + maxWidth = width; + } } + GL.Disable(EnableCap.ScissorTest); + + width = _fontService.DrawText("Item", xOffset, Height - TitleFontHeight, TitleFontHeight); + if (width > maxWidth) + maxWidth = width; + + xOffset += maxWidth + ColumnSpacing; + _buttons[(int)ButtonIndex.TagTitle].UpdateSize(0, Height - TitleFontHeight, 0, (int)xOffset, TitleFontHeight); + #endregion + + // Timing data + timingWidth = Width - xOffset - 370; + timingDataLeft = xOffset; + + GL.Scissor((int)xOffset, FilterHeight, (int)timingWidth, Height - TitleHeight - FilterHeight); + + if (_displayGraph) + { + DrawGraph(xOffset, yOffset, timingWidth); + } + else + { + DrawBars(xOffset, yOffset, timingWidth); + } + + GL.Scissor(0, FilterHeight, Width, Height - TitleHeight - FilterHeight); + + if (!_displayGraph) + { + _fontService.DrawText("Blue: Instant, Green: Avg, Red: Total", xOffset, Height - TitleFontHeight, TitleFontHeight); + } + + xOffset = Width - 360; + + // Display timestamps + #region Timestamps + verticalIndex = 0; + GL.Enable(EnableCap.ScissorTest); + foreach (var entry in _sortedProfileData) + { + float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); + _fontService.DrawText($"{Profile.ConvertTicksToMS(entry.Value.Instant):F3} ({entry.Value.InstantCount})", xOffset, y, LineHeight); + _fontService.DrawText($"{Profile.ConvertTicksToMS(entry.Value.AverageTime):F3}", 150 + xOffset, y, LineHeight); + _fontService.DrawText($"{Profile.ConvertTicksToMS(entry.Value.TotalTime):F3}", 260 + xOffset, y, LineHeight); + } + GL.Disable(EnableCap.ScissorTest); + + float yHeight = Height - TitleFontHeight; + + _fontService.DrawText("Instant (ms, count)", xOffset, yHeight, TitleFontHeight); + _buttons[(int)ButtonIndex.InstantTitle].UpdateSize((int)xOffset, (int)yHeight, 0, 130, TitleFontHeight); + + _fontService.DrawText("Average (ms)", 150 + xOffset, yHeight, TitleFontHeight); + _buttons[(int)ButtonIndex.AverageTitle].UpdateSize((int)(150 + xOffset), (int)yHeight, 0, 130, TitleFontHeight); + + _fontService.DrawText("Total (ms)", 260 + xOffset, yHeight, TitleFontHeight); + _buttons[(int)ButtonIndex.TotalTitle].UpdateSize((int)(260 + xOffset), (int)yHeight, 0, Width, TitleFontHeight); + #endregion } - GL.Disable(EnableCap.ScissorTest); - - width = _fontService.DrawText("Item", xOffset, Height - TitleFontHeight, TitleFontHeight); - if (width > maxWidth) - maxWidth = width; - - xOffset += maxWidth + ColumnSpacing; - _buttons[(int)ButtonIndex.TagTitle].UpdateSize(0, Height - TitleFontHeight, 0, (int)xOffset, TitleFontHeight); - #endregion - - // Time bars - if (_displayGraph) - { - DrawGraph(xOffset, yOffset); - } - else - { - DrawBars(xOffset, yOffset); - } - - _fontService.DrawText("Blue: Instant, Green: Avg, Red: Total", xOffset, Height - TitleFontHeight, TitleFontHeight); - xOffset = Width - 360; - - // Display timestamps - - #region Timestamps - verticalIndex = 0; - GL.Enable(EnableCap.ScissorTest); - foreach (var entry in _sortedProfileData) - { - float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); - _fontService.DrawText($"{Profile.ConvertTicksToMS(entry.Value.Instant):F3} ({entry.Value.InstantCount})", xOffset, y, LineHeight); - _fontService.DrawText($"{Profile.ConvertTicksToMS(entry.Value.AverageTime):F3}", ColumnSpacing + 120 + xOffset, y, LineHeight); - _fontService.DrawText($"{Profile.ConvertTicksToMS(entry.Value.TotalTime):F3}", ColumnSpacing + ColumnSpacing + 200 + xOffset, y, LineHeight); - } - GL.Disable(EnableCap.ScissorTest); - - float yHeight = Height - TitleFontHeight; - - _fontService.DrawText("Instant (ms, count)", xOffset, yHeight, TitleFontHeight); - _buttons[(int)ButtonIndex.InstantTitle].UpdateSize((int)xOffset, (int)yHeight, 0, (int)(ColumnSpacing + 100), TitleFontHeight); - - _fontService.DrawText("Average (ms)", ColumnSpacing + 120 + xOffset, yHeight, TitleFontHeight); - _buttons[(int)ButtonIndex.AverageTitle].UpdateSize((int)(ColumnSpacing + 120 + xOffset), (int)yHeight, 0, (int)(ColumnSpacing + 100), TitleFontHeight); - - _fontService.DrawText("Total (ms)", ColumnSpacing + ColumnSpacing + 200 + xOffset, yHeight, TitleFontHeight); - _buttons[(int)ButtonIndex.TotalTitle].UpdateSize((int)(ColumnSpacing + ColumnSpacing + 200 + xOffset), (int)yHeight, 0, Width, TitleFontHeight); - #endregion #region Bottom bar // Show/Hide Inactive @@ -495,6 +511,16 @@ namespace Ryujinx.Profiler.UI GL.Vertex2(width + 30, 0); GL.Vertex2(width + 30, FilterHeight); + + // Column dividers + float timingDataTop = Height - TitleHeight; + + GL.Vertex2(timingDataLeft, FilterHeight); + GL.Vertex2(timingDataLeft, timingDataTop); + + + GL.Vertex2(timingWidth + timingDataLeft, FilterHeight); + GL.Vertex2(timingWidth + timingDataLeft, timingDataTop); GL.End(); #endregion diff --git a/Ryujinx.Profiler/UI/ProfileWindowBars.cs b/Ryujinx.Profiler/UI/ProfileWindowBars.cs index fee9b17b9c..978c4bedc5 100644 --- a/Ryujinx.Profiler/UI/ProfileWindowBars.cs +++ b/Ryujinx.Profiler/UI/ProfileWindowBars.cs @@ -7,7 +7,7 @@ namespace Ryujinx.Profiler.UI { public partial class ProfileWindow { - private void DrawBars(float xOffset, float yOffset) + private void DrawBars(float xOffset, float yOffset, float width) { if (_sortedProfileData.Count != 0) { @@ -16,7 +16,6 @@ namespace Ryujinx.Profiler.UI int verticalIndex = 0; float barHeight = (LineHeight - LinePadding) / 3.0f; - float width = Width - xOffset - 370; // Get max values var maxInstant = maxAverage = maxTotal = 0; diff --git a/Ryujinx.Profiler/UI/ProfileWindowGraph.cs b/Ryujinx.Profiler/UI/ProfileWindowGraph.cs index b26b0643e6..6f28478836 100644 --- a/Ryujinx.Profiler/UI/ProfileWindowGraph.cs +++ b/Ryujinx.Profiler/UI/ProfileWindowGraph.cs @@ -8,73 +8,72 @@ namespace Ryujinx.Profiler.UI { public partial class ProfileWindow { - private void DrawGraph(float xOffset, float yOffset) + private void DrawGraph(float xOffset, float yOffset, float width) { if (_sortedProfileData.Count != 0) { - long maxAverage; - long maxTotal; + int left, right; + float top, bottom; - int verticalIndex = 0; - float barHeight = (LineHeight - LinePadding) / 3.0f; - float width = Width - xOffset - 370; - - // Get max values - var maxInstant = maxAverage = maxTotal = 0; - foreach (KeyValuePair kvp in _sortedProfileData) - { - maxInstant = Math.Max(maxInstant, kvp.Value.Instant); - maxAverage = Math.Max(maxAverage, kvp.Value.AverageTime); - maxTotal = Math.Max(maxTotal, kvp.Value.TotalTime); - } + int verticalIndex = 0; + float barHeight = (LineHeight - LinePadding); + long timeWidth = Profile.HistoryLength; GL.Enable(EnableCap.ScissorTest); GL.Begin(PrimitiveType.Triangles); foreach (var entry in _sortedProfileData) { - // Instant - GL.Color3(Color.Purple); - float bottom = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++); - float top = bottom + barHeight; - float right = (float)entry.Value.Instant / maxInstant * width + xOffset; + GL.Color3(Color.Green); + foreach (Timestamp timestamp in entry.Value.GetAllTimestamps()) + { + left = (int)(xOffset + width - (((float)_captureTime - timestamp.BeginTime) / timeWidth) * width); + right = (int)(xOffset + width - (((float)_captureTime - timestamp.EndTime) / timeWidth) * width); + bottom = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex); + top = bottom + barHeight; - // Skip rendering out of bounds bars - if (top < 0 || bottom > Height) - continue; + // Make sure width is at least 1px + right = Math.Max(left + 1, right); - GL.Vertex2(xOffset, bottom); - GL.Vertex2(xOffset, top); - GL.Vertex2(right, top); + // Skip rendering out of bounds bars + if (top < 0 || bottom > Height) + continue; - GL.Vertex2(right, top); - GL.Vertex2(right, bottom); - GL.Vertex2(xOffset, bottom); + GL.Vertex2(left, bottom); + GL.Vertex2(left, top); + GL.Vertex2(right, top); - // Average - GL.Color3(Color.Purple); - top += barHeight; - bottom += barHeight; - right = (float)entry.Value.AverageTime / maxAverage * width + xOffset; - GL.Vertex2(xOffset, bottom); - GL.Vertex2(xOffset, top); - GL.Vertex2(right, top); + GL.Vertex2(right, top); + GL.Vertex2(right, bottom); + GL.Vertex2(left, bottom); + } - GL.Vertex2(right, top); - GL.Vertex2(right, bottom); - GL.Vertex2(xOffset, bottom); + GL.Color3(Color.Red); + // Currently capturing timestamp + long entryBegin = entry.Value.BeginTime; + if (entryBegin != -1) + { + left = (int)(xOffset + width - (((float)_captureTime - entryBegin) / timeWidth) * width); + bottom = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex); + top = bottom + barHeight; + right = (int)(xOffset + width); - // Total - GL.Color3(Color.Purple); - top += barHeight; - bottom += barHeight; - right = (float)entry.Value.TotalTime / maxTotal * width + xOffset; - GL.Vertex2(xOffset, bottom); - GL.Vertex2(xOffset, top); - GL.Vertex2(right, top); + // Make sure width is at least 1px + left = Math.Min(left - 1, right); - GL.Vertex2(right, top); - GL.Vertex2(right, bottom); - GL.Vertex2(xOffset, bottom); + // Skip rendering out of bounds bars + if (top < 0 || bottom > Height) + continue; + + GL.Vertex2(left, bottom); + GL.Vertex2(left, top); + GL.Vertex2(right, top); + + GL.Vertex2(right, top); + GL.Vertex2(right, bottom); + GL.Vertex2(left, bottom); + } + + verticalIndex++; } GL.End();