Realtime graphing initial commit

This commit is contained in:
Andy Adshead 2019-01-28 23:21:46 +00:00
parent 1b076a6836
commit 8bad1e21e5
6 changed files with 270 additions and 203 deletions

View file

@ -9,6 +9,8 @@ namespace Ryujinx.Profiler
{
public class InternalProfile
{
public long CurrentTime => SW.ElapsedTicks;
private Stopwatch SW;
internal ConcurrentDictionary<ProfileConfig, TimingInfo> Timers;

View file

@ -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;
}
}
}

View file

@ -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<Timestamp> Timestamps;
private readonly object timestampLock = new object();
private Timestamp currentTimestamp;
private List<Timestamp> _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<Timestamp>();
depth = 0;
_timestamps = new List<Timestamp>();
_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;
}
}
}

View file

@ -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<KeyValuePair<ProfileConfig, TimingInfo>> _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

View file

@ -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;

View file

@ -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<ProfileConfig, TimingInfo> 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();