diff --git a/Ryujinx.Profiler/DumpProfile.cs b/Ryujinx.Profiler/DumpProfile.cs new file mode 100644 index 0000000000..dd8b5718a4 --- /dev/null +++ b/Ryujinx.Profiler/DumpProfile.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; + +namespace Ryujinx.Profiler +{ + public static class DumpProfile + { + public static void ToFile(string path, InternalProfile profile) + { + String fileData = ""; + foreach (var time in profile.Timers) + { + fileData += $"{time.Key} - " + + $"Total: {profile.ConvertTicksToMS(time.Value.TotalTime)}ms, " + + $"Average: {profile.ConvertTicksToMS(time.Value.AverageTime)}ms, " + + $"Count: {time.Value.Count}\n"; + } + + // Ensure file directory exists before write + FileInfo fileInfo = new FileInfo(path); + if (fileInfo == null) + throw new Exception("Unknown logging error, probably a bad file path"); + if (fileInfo.Directory != null && !fileInfo.Directory.Exists) + Directory.CreateDirectory(fileInfo.Directory.FullName); + + File.WriteAllText(fileInfo.FullName, fileData); + } + } +} diff --git a/Ryujinx.Profiler/InternalProfile.cs b/Ryujinx.Profiler/InternalProfile.cs new file mode 100644 index 0000000000..9a458edd85 --- /dev/null +++ b/Ryujinx.Profiler/InternalProfile.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; + +namespace Ryujinx.Profiler +{ + public class InternalProfile + { + private Stopwatch SW; + internal ConcurrentDictionary Timers; + + public InternalProfile() + { + Timers = new ConcurrentDictionary(); + SW = new Stopwatch(); + SW.Start(); + } + + public void BeginProfile(ProfileConfig config) + { + long timestamp = SW.ElapsedTicks; + + Timers.AddOrUpdate(config.Name, + (string s) => CreateTimer(timestamp), + ((s, info) => + { + info.BeginTime = timestamp; + return info; + })); + } + + public void EndProfile(ProfileConfig config) + { + long timestamp = SW.ElapsedTicks; + + Timers.AddOrUpdate(config.Name, + (s => new TimingInfo()), + ((s, time) => UpdateTimer(time, timestamp))); + } + + private TimingInfo CreateTimer(long timestamp) + { + return new TimingInfo() + { + BeginTime = timestamp, + LastTime = 0, + Count = 0, + }; + } + + private TimingInfo UpdateTimer(TimingInfo time, long timestamp) + { + time.Count++; + time.LastTime = timestamp - time.BeginTime; + time.TotalTime += time.LastTime; + + return time; + } + + public double ConvertTicksToMS(long ticks) + { + return (((double)ticks) / Stopwatch.Frequency) * 1000.0; + } + } +} diff --git a/Ryujinx.Profiler/Profile.cs b/Ryujinx.Profiler/Profile.cs index 66e0204e2d..2754f165a2 100644 --- a/Ryujinx.Profiler/Profile.cs +++ b/Ryujinx.Profiler/Profile.cs @@ -1,23 +1,11 @@ using System; -using System.Collections.Concurrent; -using System.Diagnostics; namespace Ryujinx.Profiler { public class Profile { - private struct TimingInfo - { - public long BeginTime, LastTime, TotalTime, Count; - public long AverageTime - { - get => (Count == 0) ? -1 : TotalTime / Count; - } - } - - // Static - private static Profile ProfileInstance; + private static InternalProfile ProfileInstance; private static ProfilerSettings Settings; private static bool ProfilingEnabled() @@ -26,7 +14,7 @@ namespace Ryujinx.Profiler return false; if (ProfileInstance == null) - ProfileInstance = new Profile(); + ProfileInstance = new InternalProfile(); return true; } @@ -35,6 +23,15 @@ namespace Ryujinx.Profiler Settings = settings; } + public static void FinishProfiling() + { + if (!ProfilingEnabled()) + return; + + if (Settings.FileDumpEnabled) + DumpProfile.ToFile(Settings.DumpLocation, ProfileInstance); + } + public static void Begin(ProfileConfig config) { if (!ProfilingEnabled()) @@ -59,58 +56,5 @@ namespace Ryujinx.Profiler method(); End(config); } - - - // Non-static - private Stopwatch SW; - private ConcurrentDictionary Timers; - - public Profile() - { - Timers = new ConcurrentDictionary(); - SW = new Stopwatch(); - SW.Start(); - } - - public void BeginProfile(ProfileConfig config) - { - long timestamp = SW.ElapsedTicks; - - Timers.AddOrUpdate(config.Name, - (string s) => CreateTimer(timestamp), - ((s, info) => - { - info.BeginTime = timestamp; - return info; - })); - } - - public void EndProfile(ProfileConfig config) - { - long timestamp = SW.ElapsedTicks; - - Timers.AddOrUpdate(config.Name, - (s => new TimingInfo()), - ((s, time) => UpdateTimer(time, timestamp))); - } - - private TimingInfo CreateTimer(long timestamp) - { - return new TimingInfo() - { - BeginTime = timestamp, - LastTime = 0, - Count = 0, - }; - } - - private TimingInfo UpdateTimer(TimingInfo time, long timestamp) - { - time.Count++; - time.LastTime = timestamp - time.BeginTime; - time.TotalTime += time.LastTime; - - return time; - } } } diff --git a/Ryujinx.Profiler/TimingInfo.cs b/Ryujinx.Profiler/TimingInfo.cs new file mode 100644 index 0000000000..d9c3f7f2fb --- /dev/null +++ b/Ryujinx.Profiler/TimingInfo.cs @@ -0,0 +1,11 @@ +namespace Ryujinx.Profiler +{ + public struct TimingInfo + { + public long BeginTime, LastTime, TotalTime, Count; + public long AverageTime + { + get => (Count == 0) ? -1 : TotalTime / Count; + } + } +} diff --git a/Ryujinx/Ui/GLScreen.cs b/Ryujinx/Ui/GLScreen.cs index e3a6d299bc..478c5887f2 100644 --- a/Ryujinx/Ui/GLScreen.cs +++ b/Ryujinx/Ui/GLScreen.cs @@ -269,6 +269,9 @@ namespace Ryujinx _renderThread.Join(); base.OnUnload(e); + + // TODO: Find a better home for this, currently only called on OGL window close + Profile.FinishProfiling(); } protected override void OnResize(EventArgs e)