using Gtk; using Ryujinx.Common; using Ryujinx.Debugger.Profiler; using SkiaSharp; using SkiaSharp.Views.Desktop; using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using GUI = Gtk.Builder.ObjectAttribute; namespace Ryujinx.Debugger.UI { public class ProfilerWidget : Box { private Thread _profilerThread; private double _prevTime; private bool _profilerRunning; private TimingFlag[] _timingFlags; private bool _initComplete = false; private bool _redrawPending = true; private bool _doStep = false; // Layout private const int LineHeight = 16; private const int MinimumColumnWidth = 200; private const int TitleHeight = 24; private const int TitleFontHeight = 16; private const int LinePadding = 2; private const int ColumnSpacing = 15; private const int FilterHeight = 24; private const int BottomBarHeight = FilterHeight + LineHeight; // Sorting private List> _unsortedProfileData; private IComparer> _sortAction = new ProfileSorters.TagAscending(); // Flag data private long[] _timingFlagsAverages; private long[] _timingFlagsLast; // Filtering private string _filterText = ""; private bool _regexEnabled = false; // Scrolling private float _scrollPos = 0; // Profile data storage private List> _sortedProfileData; private long _captureTime; // Graph private SKColor[] _timingFlagColors = new[] { new SKColor(150, 25, 25, 50), // FrameSwap = 0 new SKColor(25, 25, 150, 50), // SystemFrame = 1 }; private const float GraphMoveSpeed = 40000; private const float GraphZoomSpeed = 50; private float _graphZoom = 1; private float _graphPosition = 0; private int _rendererHeight => _renderer.AllocatedHeight; private int _rendererWidth => _renderer.AllocatedWidth; // Event management private long _lastOutputUpdate; private long _lastOutputDraw; private long _lastOutputUpdateDuration; private long _lastOutputDrawDuration; private double _lastFrameTimeMs; private double _updateTimer; private bool _profileUpdated = false; private readonly object _profileDataLock = new object(); private SkRenderer _renderer; #pragma warning disable CS0649 [GUI] ScrolledWindow _scrollview; [GUI] CheckButton _enableCheckbutton; [GUI] Scrollbar _outputScrollbar; [GUI] Entry _filterBox; [GUI] ComboBox _modeBox; [GUI] CheckButton _showFlags; [GUI] CheckButton _showInactive; [GUI] Button _stepButton; [GUI] CheckButton _pauseCheckbutton; #pragma warning restore CS0649 public ProfilerWidget() : this(new Builder("Ryujinx.Debugger.UI.ProfilerWidget.glade")) { } public ProfilerWidget(Builder builder) : base(builder.GetObject("_profilerBox").Handle) { builder.Autoconnect(this); this.KeyPressEvent += ProfilerWidget_KeyPressEvent; this.Expand = true; _renderer = new SkRenderer(); _renderer.Expand = true; _outputScrollbar.ValueChanged += _outputScrollbar_ValueChanged; _renderer.DrawGraphs += _renderer_DrawGraphs; _filterBox.Changed += _filterBox_Changed; _stepButton.Clicked += _stepButton_Clicked; _scrollview.Add(_renderer); if (Profile.UpdateRate <= 0) { // Perform step regardless of flag type Profile.RegisterFlagReceiver((t) => { if (_pauseCheckbutton.Active) { _doStep = true; } }); } } private void _stepButton_Clicked(object sender, EventArgs e) { if (_pauseCheckbutton.Active) { _doStep = true; } _profileUpdated = true; } private void _filterBox_Changed(object sender, EventArgs e) { _filterText = _filterBox.Text; _profileUpdated = true; } private void _outputScrollbar_ValueChanged(object sender, EventArgs e) { _scrollPos = -(float)Math.Max(0, _outputScrollbar.Value); _profileUpdated = true; } private void _renderer_DrawGraphs(object sender, EventArgs e) { if (e is SKPaintSurfaceEventArgs se) { Draw(se.Surface.Canvas); } } public void RegisterParentDebugger(DebuggerWidget debugger) { debugger.DebuggerEnabled += Debugger_DebuggerAttached; debugger.DebuggerDisabled += Debugger_DebuggerDettached; } private void Debugger_DebuggerDettached(object sender, EventArgs e) { _profilerRunning = false; if (_profilerThread != null) { _profilerThread.Join(); } } private void Debugger_DebuggerAttached(object sender, EventArgs e) { _profilerRunning = false; if (_profilerThread != null) { _profilerThread.Join(); } _profilerRunning = true; _profilerThread = new Thread(UpdateLoop) { Name = "Profiler.UpdateThread" }; _profilerThread.Start(); } private void ProfilerWidget_KeyPressEvent(object o, Gtk.KeyPressEventArgs args) { switch (args.Event.Key) { case Gdk.Key.Left: _graphPosition += (long)(GraphMoveSpeed * _lastFrameTimeMs); break; case Gdk.Key.Right: _graphPosition = Math.Max(_graphPosition - (long)(GraphMoveSpeed * _lastFrameTimeMs), 0); break; case Gdk.Key.Up: _graphZoom = MathF.Min(_graphZoom + (float)(GraphZoomSpeed * _lastFrameTimeMs), 100.0f); break; case Gdk.Key.Down: _graphZoom = MathF.Max(_graphZoom - (float)(GraphZoomSpeed * _lastFrameTimeMs), 1f); break; } _profileUpdated = true; } public void UpdateLoop() { _lastOutputUpdate = PerformanceCounter.ElapsedTicks; _lastOutputDraw = PerformanceCounter.ElapsedTicks; while (_profilerRunning) { _lastOutputUpdate = PerformanceCounter.ElapsedTicks; int timeToSleepMs = (_pauseCheckbutton.Active || !_enableCheckbutton.Active) ? 33 : 1; if (Profile.ProfilingEnabled() && _enableCheckbutton.Active) { double time = (double)PerformanceCounter.ElapsedTicks / PerformanceCounter.TicksPerSecond; Update(time - _prevTime); _lastOutputUpdateDuration = PerformanceCounter.ElapsedTicks - _lastOutputUpdate; _prevTime = time; Gdk.Threads.AddIdle(1000, ()=> { _renderer.QueueDraw(); return true; }); } Thread.Sleep(timeToSleepMs); } } public void Update(double frameTime) { _lastFrameTimeMs = frameTime; // Get timing data if enough time has passed _updateTimer += frameTime; if (_doStep || ((Profile.UpdateRate > 0) && (!_pauseCheckbutton.Active && (_updateTimer > Profile.UpdateRate)))) { _updateTimer = 0; _captureTime = PerformanceCounter.ElapsedTicks; _timingFlags = Profile.GetTimingFlags(); _doStep = false; _profileUpdated = true; _unsortedProfileData = Profile.GetProfilingData(); (_timingFlagsAverages, _timingFlagsLast) = Profile.GetTimingAveragesAndLast(); } // Filtering if (_profileUpdated) { lock (_profileDataLock) { _sortedProfileData = _showInactive.Active ? _unsortedProfileData : _unsortedProfileData.FindAll(kvp => kvp.Value.IsActive); if (_sortAction != null) { _sortedProfileData.Sort(_sortAction); } if (_regexEnabled) { try { Regex filterRegex = new Regex(_filterText, RegexOptions.IgnoreCase); if (_filterText != "") { _sortedProfileData = _sortedProfileData.Where((pair => filterRegex.IsMatch(pair.Key.Search))).ToList(); } } catch (ArgumentException) { // Skip filtering for invalid regex } } else { // Regular filtering _sortedProfileData = _sortedProfileData.Where((pair => pair.Key.Search.ToLower().Contains(_filterText.ToLower()))).ToList(); } } _profileUpdated = false; _redrawPending = true; _initComplete = true; } } private string GetTimeString(long timestamp) { float time = (float)timestamp / PerformanceCounter.TicksPerMillisecond; return (time < 1) ? $"{time * 1000:F3}us" : $"{time:F3}ms"; } private void FilterBackspace() { if (_filterText.Length <= 1) { _filterText = ""; } else { _filterText = _filterText.Remove(_filterText.Length - 1, 1); } } private float GetLineY(float offset, float lineHeight, float padding, bool centre, int line) { return offset + lineHeight + padding + ((lineHeight + padding) * line) - ((centre) ? padding : 0); } public void Draw(SKCanvas canvas) { _lastOutputDraw = PerformanceCounter.ElapsedTicks; if (!Visible || !_initComplete || !_enableCheckbutton.Active || !_redrawPending) { return; } float viewTop = TitleHeight + 5; float viewBottom = _rendererHeight - FilterHeight - LineHeight; float columnWidth; float maxColumnWidth = MinimumColumnWidth; float yOffset = _scrollPos + viewTop; float xOffset = 10; float timingWidth; float contentHeight = GetLineY(0, LineHeight, LinePadding, false, _sortedProfileData.Count - 1); _outputScrollbar.Adjustment.Upper = contentHeight; _outputScrollbar.Adjustment.Lower = 0; _outputScrollbar.Adjustment.PageSize = viewBottom - viewTop; SKPaint textFont = new SKPaint() { Color = SKColors.White, TextSize = LineHeight }; SKPaint titleFont = new SKPaint() { Color = SKColors.White, TextSize = TitleFontHeight }; SKPaint evenItemBackground = new SKPaint() { Color = SKColors.Gray }; canvas.Save(); canvas.ClipRect(new SKRect(0, viewTop, _rendererWidth, viewBottom), SKClipOperation.Intersect); for (int i = 1; i < _sortedProfileData.Count; i += 2) { float top = GetLineY(yOffset, LineHeight, LinePadding, false, i - 1); float bottom = GetLineY(yOffset, LineHeight, LinePadding, false, i); canvas.DrawRect(new SKRect(0, top, _rendererWidth, bottom), evenItemBackground); } lock (_profileDataLock) { // Display category for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++) { KeyValuePair entry = _sortedProfileData[verticalIndex]; if (entry.Key.Category == null) { continue; } float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex); canvas.DrawText(entry.Key.Category, new SKPoint(xOffset, y), textFont); columnWidth = textFont.MeasureText(entry.Key.Category); if (columnWidth > maxColumnWidth) { maxColumnWidth = columnWidth; } } canvas.Restore(); canvas.DrawText("Category", new SKPoint(xOffset, TitleFontHeight + 2), titleFont); columnWidth = titleFont.MeasureText("Category"); if (columnWidth > maxColumnWidth) { maxColumnWidth = columnWidth; } xOffset += maxColumnWidth + ColumnSpacing; canvas.DrawLine(new SKPoint(xOffset - ColumnSpacing / 2, 0), new SKPoint(xOffset - ColumnSpacing / 2, viewBottom), textFont); // Display session group maxColumnWidth = MinimumColumnWidth; canvas.Save(); canvas.ClipRect(new SKRect(0, viewTop, _rendererWidth, viewBottom), SKClipOperation.Intersect); for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++) { KeyValuePair entry = _sortedProfileData[verticalIndex]; if (entry.Key.SessionGroup == null) { continue; } float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex); canvas.DrawText(entry.Key.SessionGroup, new SKPoint(xOffset, y), textFont); columnWidth = textFont.MeasureText(entry.Key.SessionGroup); if (columnWidth > maxColumnWidth) { maxColumnWidth = columnWidth; } } canvas.Restore(); canvas.DrawText("Group", new SKPoint(xOffset, TitleFontHeight + 2), titleFont); columnWidth = titleFont.MeasureText("Group"); if (columnWidth > maxColumnWidth) { maxColumnWidth = columnWidth; } xOffset += maxColumnWidth + ColumnSpacing; canvas.DrawLine(new SKPoint(xOffset - ColumnSpacing / 2, 0), new SKPoint(xOffset - ColumnSpacing / 2, viewBottom), textFont); // Display session item maxColumnWidth = MinimumColumnWidth; canvas.Save(); canvas.ClipRect(new SKRect(0, viewTop, _rendererWidth, viewBottom), SKClipOperation.Intersect); for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++) { KeyValuePair entry = _sortedProfileData[verticalIndex]; if (entry.Key.SessionItem == null) { continue; } float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex); canvas.DrawText(entry.Key.SessionItem, new SKPoint(xOffset, y), textFont); columnWidth = textFont.MeasureText(entry.Key.SessionItem); if (columnWidth > maxColumnWidth) { maxColumnWidth = columnWidth; } } canvas.Restore(); canvas.DrawText("Item", new SKPoint(xOffset, TitleFontHeight + 2), titleFont); columnWidth = titleFont.MeasureText("Item"); if (columnWidth > maxColumnWidth) { maxColumnWidth = columnWidth; } xOffset += maxColumnWidth + ColumnSpacing; timingWidth = _rendererWidth - xOffset - 370; canvas.Save(); canvas.ClipRect(new SKRect(0, viewTop, _rendererWidth, viewBottom), SKClipOperation.Intersect); canvas.DrawLine(new SKPoint(xOffset, 0), new SKPoint(xOffset, _rendererHeight), textFont); int mode = _modeBox.Active; canvas.Save(); canvas.ClipRect(new SKRect(xOffset, yOffset,xOffset + timingWidth,yOffset + contentHeight), SKClipOperation.Intersect); switch (mode) { case 0: DrawGraph(xOffset, yOffset, timingWidth, canvas); break; case 1: DrawBars(xOffset, yOffset, timingWidth, canvas); canvas.DrawText("Blue: Instant, Green: Avg, Red: Total", new SKPoint(xOffset, _rendererHeight - TitleFontHeight), titleFont); break; } canvas.Restore(); canvas.DrawLine(new SKPoint(xOffset + timingWidth, 0), new SKPoint(xOffset + timingWidth, _rendererHeight), textFont); xOffset = _rendererWidth - 360; // Display timestamps long totalInstant = 0; long totalAverage = 0; long totalTime = 0; long totalCount = 0; for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++) { KeyValuePair entry = _sortedProfileData[verticalIndex]; float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex); canvas.DrawText($"{GetTimeString(entry.Value.Instant)} ({entry.Value.InstantCount})", new SKPoint(xOffset, y), textFont); canvas.DrawText(GetTimeString(entry.Value.AverageTime), new SKPoint(150 + xOffset, y), textFont); canvas.DrawText(GetTimeString(entry.Value.TotalTime), new SKPoint(260 + xOffset, y), textFont); totalInstant += entry.Value.Instant; totalAverage += entry.Value.AverageTime; totalTime += entry.Value.TotalTime; totalCount += entry.Value.InstantCount; } canvas.Restore(); canvas.DrawLine(new SKPoint(0, viewTop), new SKPoint(_rendererWidth, viewTop), titleFont); float yHeight = 0 + TitleFontHeight; canvas.DrawText("Instant (Count)", new SKPoint(xOffset, yHeight), titleFont); canvas.DrawText("Average", new SKPoint(150 + xOffset, yHeight), titleFont); canvas.DrawText("Total (ms)", new SKPoint(260 + xOffset, yHeight), titleFont); // Totals yHeight = _rendererHeight - FilterHeight + 3; int textHeight = LineHeight - 2; SKPaint detailFont = new SKPaint() { Color = new SKColor(100, 100, 255, 255), TextSize = textHeight }; canvas.DrawLine(new SkiaSharp.SKPoint(0, viewBottom), new SkiaSharp.SKPoint(_rendererWidth,viewBottom), textFont); string hostTimeString = $"Host {GetTimeString(_timingFlagsLast[(int)TimingFlagType.SystemFrame])} " + $"({GetTimeString(_timingFlagsAverages[(int)TimingFlagType.SystemFrame])})"; canvas.DrawText(hostTimeString, new SKPoint(5, yHeight), detailFont); float tempWidth = detailFont.MeasureText(hostTimeString); detailFont.Color = SKColors.Red; string gameTimeString = $"Game {GetTimeString(_timingFlagsLast[(int)TimingFlagType.FrameSwap])} " + $"({GetTimeString(_timingFlagsAverages[(int)TimingFlagType.FrameSwap])})"; canvas.DrawText(gameTimeString, new SKPoint(15 + tempWidth, yHeight), detailFont); tempWidth += detailFont.MeasureText(gameTimeString); detailFont.Color = SKColors.White; canvas.DrawText($"Profiler: Update {GetTimeString(_lastOutputUpdateDuration)} Draw {GetTimeString(_lastOutputDrawDuration)}", new SKPoint(20 + tempWidth, yHeight), detailFont); detailFont.Color = SKColors.White; canvas.DrawText($"{GetTimeString(totalInstant)} ({totalCount})", new SKPoint(xOffset, yHeight), detailFont); canvas.DrawText(GetTimeString(totalAverage), new SKPoint(150 + xOffset, yHeight), detailFont); canvas.DrawText(GetTimeString(totalTime), new SKPoint(260 + xOffset, yHeight), detailFont); _lastOutputDrawDuration = PerformanceCounter.ElapsedTicks - _lastOutputDraw; } } private void DrawGraph(float xOffset, float yOffset, float width, SKCanvas canvas) { if (_sortedProfileData.Count != 0) { int left, right; float top, bottom; float graphRight = xOffset + width; float barHeight = (LineHeight - LinePadding); long history = Profile.HistoryLength; double timeWidthTicks = history / (double)_graphZoom; long graphPositionTicks = (long)(_graphPosition * PerformanceCounter.TicksPerMillisecond); long ticksPerPixel = (long)(timeWidthTicks / width); // Reset start point if out of bounds if (timeWidthTicks + graphPositionTicks > history) { graphPositionTicks = history - (long)timeWidthTicks; _graphPosition = (float)graphPositionTicks / PerformanceCounter.TicksPerMillisecond; } graphPositionTicks = _captureTime - graphPositionTicks; // Draw timing flags if (_showFlags.Active) { TimingFlagType prevType = TimingFlagType.Count; SKPaint timingPaint = new SKPaint { Color = _timingFlagColors.First() }; foreach (TimingFlag timingFlag in _timingFlags) { if (prevType != timingFlag.FlagType) { prevType = timingFlag.FlagType; timingPaint.Color = _timingFlagColors[(int)prevType]; } int x = (int)(graphRight - ((graphPositionTicks - timingFlag.Timestamp) / timeWidthTicks) * width); if (x > xOffset) { canvas.DrawLine(new SKPoint(x, yOffset), new SKPoint(x, _rendererHeight), timingPaint); } } } SKPaint barPaint = new SKPaint() { Color = SKColors.Green, }; // Draw bars for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++) { KeyValuePair entry = _sortedProfileData[verticalIndex]; long furthest = 0; bottom = GetLineY(yOffset, LineHeight, LinePadding, false, verticalIndex); top = bottom + barHeight; // Skip rendering out of bounds bars if (top < 0 || bottom > _rendererHeight) { continue; } barPaint.Color = SKColors.Green; foreach (Timestamp timestamp in entry.Value.GetAllTimestamps()) { // Skip drawing multiple timestamps on same pixel if (timestamp.EndTime < furthest) { continue; } furthest = timestamp.EndTime + ticksPerPixel; left = (int)(graphRight - ((graphPositionTicks - timestamp.BeginTime) / timeWidthTicks) * width); right = (int)(graphRight - ((graphPositionTicks - timestamp.EndTime) / timeWidthTicks) * width); left = (int)Math.Max(xOffset +1, left); // Make sure width is at least 1px right = Math.Max(left + 1, right); canvas.DrawRect(new SKRect(left, top, right, bottom), barPaint); } // Currently capturing timestamp barPaint.Color = SKColors.Red; long entryBegin = entry.Value.BeginTime; if (entryBegin != -1) { left = (int)(graphRight - ((graphPositionTicks - entryBegin) / timeWidthTicks) * width); // Make sure width is at least 1px left = Math.Min(left - 1, (int)graphRight); left = (int)Math.Max(xOffset + 1, left); canvas.DrawRect(new SKRect(left, top, graphRight, bottom), barPaint); } } string label = $"-{MathF.Round(_graphPosition, 2)} ms"; SKPaint labelPaint = new SKPaint() { Color = SKColors.White, TextSize = LineHeight }; float labelWidth = labelPaint.MeasureText(label); canvas.DrawText(label,new SKPoint(graphRight - labelWidth - LinePadding, FilterHeight + LinePadding) , labelPaint); canvas.DrawText($"-{MathF.Round((float)((timeWidthTicks / PerformanceCounter.TicksPerMillisecond) + _graphPosition), 2)} ms", new SKPoint(xOffset + LinePadding, FilterHeight + LinePadding), labelPaint); } } private void DrawBars(float xOffset, float yOffset, float width, SKCanvas canvas) { if (_sortedProfileData.Count != 0) { long maxAverage = 0; long maxTotal = 0; long maxInstant = 0; float barHeight = (LineHeight - LinePadding) / 3.0f; // Get max values 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); } SKPaint barPaint = new SKPaint() { Color = SKColors.Blue }; for (int verticalIndex = 0; verticalIndex < _sortedProfileData.Count; verticalIndex++) { KeyValuePair entry = _sortedProfileData[verticalIndex]; // Instant barPaint.Color = SKColors.Blue; float bottom = GetLineY(yOffset, LineHeight, LinePadding, false, verticalIndex); float top = bottom + barHeight; float right = (float)entry.Value.Instant / maxInstant * width + xOffset; // Skip rendering out of bounds bars if (top < 0 || bottom > _rendererHeight) { continue; } canvas.DrawRect(new SKRect(xOffset, top, right, bottom), barPaint); // Average barPaint.Color = SKColors.Green; top += barHeight; bottom += barHeight; right = (float)entry.Value.AverageTime / maxAverage * width + xOffset; canvas.DrawRect(new SKRect(xOffset, top, right, bottom), barPaint); // Total barPaint.Color = SKColors.Red; top += barHeight; bottom += barHeight; right = (float)entry.Value.TotalTime / maxTotal * width + xOffset; canvas.DrawRect(new SKRect(xOffset, top, right, bottom), barPaint); } } } } }