diff --git a/ChocolArm64/ChocolArm64.csproj b/ChocolArm64/ChocolArm64.csproj
index 0b4051b0..ea98003f 100644
--- a/ChocolArm64/ChocolArm64.csproj
+++ b/ChocolArm64/ChocolArm64.csproj
@@ -3,19 +3,36 @@
   <PropertyGroup>
     <TargetFramework>netcoreapp2.1</TargetFramework>
     <RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
+    <Configurations>Debug;Release;Profile Debug;Profile Release</Configurations>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>false</Optimize>
+  </PropertyGroup>
+
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
   <ItemGroup>
     <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
     <PackageReference Include="System.Runtime.Intrinsics.Experimental" Version="4.5.0-rc1" />
   </ItemGroup>
 
+  <ItemGroup>
+    <ProjectReference Include="..\Ryujinx.Profiler\Ryujinx.Profiler.csproj" />
+  </ItemGroup>
+
 </Project>
diff --git a/Ryujinx.Audio/Ryujinx.Audio.csproj b/Ryujinx.Audio/Ryujinx.Audio.csproj
index 82d2a4d1..a6a34f40 100644
--- a/Ryujinx.Audio/Ryujinx.Audio.csproj
+++ b/Ryujinx.Audio/Ryujinx.Audio.csproj
@@ -3,16 +3,29 @@
   <PropertyGroup>
     <TargetFramework>netcoreapp2.1</TargetFramework>
     <RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
+    <Configurations>Debug;Release;Profile Debug;Profile Release</Configurations>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>false</Optimize>
+  </PropertyGroup>
+
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
   <ItemGroup>
     <PackageReference Include="OpenTK.NetStandard" Version="1.0.4" />
     <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.1" />
diff --git a/Ryujinx.Common/Ryujinx.Common.csproj b/Ryujinx.Common/Ryujinx.Common.csproj
index bba481e6..cf078db8 100644
--- a/Ryujinx.Common/Ryujinx.Common.csproj
+++ b/Ryujinx.Common/Ryujinx.Common.csproj
@@ -3,16 +3,29 @@
   <PropertyGroup>
     <TargetFramework>netcoreapp2.1</TargetFramework>
     <RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
+    <Configurations>Debug;Release;Profile Debug;Profile Release</Configurations>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>false</Optimize>
+  </PropertyGroup>
+
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+  
   <ItemGroup>
     <PackageReference Include="Utf8Json" Version="1.3.7" />
   </ItemGroup>
diff --git a/Ryujinx.Graphics/Ryujinx.Graphics.csproj b/Ryujinx.Graphics/Ryujinx.Graphics.csproj
index a4324715..74000895 100644
--- a/Ryujinx.Graphics/Ryujinx.Graphics.csproj
+++ b/Ryujinx.Graphics/Ryujinx.Graphics.csproj
@@ -3,16 +3,29 @@
   <PropertyGroup>
     <TargetFramework>netcoreapp2.1</TargetFramework>
     <RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
+    <Configurations>Debug;Release;Profile Debug;Profile Release</Configurations>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>false</Optimize>
+  </PropertyGroup>
+
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
   <ItemGroup>
     <PackageReference Include="FFmpeg.AutoGen" Version="4.0.0.4" />
     <PackageReference Include="OpenTK.NetStandard" Version="1.0.4" />
diff --git a/Ryujinx.HLE/HOS/Services/IpcService.cs b/Ryujinx.HLE/HOS/Services/IpcService.cs
index 2a4a9319..b93c8422 100644
--- a/Ryujinx.HLE/HOS/Services/IpcService.cs
+++ b/Ryujinx.HLE/HOS/Services/IpcService.cs
@@ -6,6 +6,7 @@ using Ryujinx.HLE.HOS.Kernel.Ipc;
 using System;
 using System.Collections.Generic;
 using System.IO;
+using Ryujinx.Profiler;
 
 namespace Ryujinx.HLE.HOS.Services
 {
@@ -101,7 +102,13 @@ namespace Ryujinx.HLE.HOS.Services
                 {
                     Logger.PrintDebug(LogClass.KernelIpc, $"{service.GetType().Name}: {processRequest.Method.Name}");
 
+                    ProfileConfig profile = Profiles.ServiceCall;
+                    profile.SessionGroup  = service.GetType().Name;
+                    profile.SessionItem   = processRequest.Method.Name;
+
+                    Profile.Begin(profile);
                     result = processRequest(context);
+                    Profile.End(profile);
                 }
                 else
                 {
@@ -203,4 +210,4 @@ namespace Ryujinx.HLE.HOS.Services
             return _domainObjects.GetData<IIpcService>(id);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/Ryujinx.HLE/PerformanceStatistics.cs b/Ryujinx.HLE/PerformanceStatistics.cs
index 408e5d72..896ab67b 100644
--- a/Ryujinx.HLE/PerformanceStatistics.cs
+++ b/Ryujinx.HLE/PerformanceStatistics.cs
@@ -1,4 +1,5 @@
-using System.Diagnostics;
+using Ryujinx.Profiler;
+using System.Diagnostics;
 using System.Timers;
 
 namespace Ryujinx.HLE
@@ -82,11 +83,13 @@ namespace Ryujinx.HLE
         public void RecordSystemFrameTime()
         {
             RecordFrameTime(FrameTypeSystem);
+            Profile.FlagTime(TimingFlagType.SystemFrame);
         }
 
         public void RecordGameFrameTime()
         {
             RecordFrameTime(FrameTypeGame);
+            Profile.FlagTime(TimingFlagType.FrameSwap);
         }
 
         private void RecordFrameTime(int frameType)
diff --git a/Ryujinx.HLE/Ryujinx.HLE.csproj b/Ryujinx.HLE/Ryujinx.HLE.csproj
index fd404863..a653b53f 100644
--- a/Ryujinx.HLE/Ryujinx.HLE.csproj
+++ b/Ryujinx.HLE/Ryujinx.HLE.csproj
@@ -3,16 +3,29 @@
   <PropertyGroup>
     <TargetFramework>netcoreapp2.1</TargetFramework>
     <RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
+    <Configurations>Debug;Release;Profile Debug;Profile Release</Configurations>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>false</Optimize>
+  </PropertyGroup>
+
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
   <ItemGroup>
     <None Remove="Homebrew.npdm" />
     <None Remove="RyujinxProfileImage.jpg" />
@@ -28,6 +41,7 @@
     <ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
     <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
     <ProjectReference Include="..\Ryujinx.Graphics\Ryujinx.Graphics.csproj" />
+    <ProjectReference Include="..\Ryujinx.Profiler\Ryujinx.Profiler.csproj" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/Ryujinx.LLE/Luea.csproj b/Ryujinx.LLE/Luea.csproj
index 5c571568..719a0ef3 100644
--- a/Ryujinx.LLE/Luea.csproj
+++ b/Ryujinx.LLE/Luea.csproj
@@ -4,6 +4,17 @@
     <TargetFramework>netcoreapp2.1</TargetFramework>
     <RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
     <OutputType>Exe</OutputType>
+    <Configurations>Debug;Release;Profile Debug;Profile Release</Configurations>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>false</Optimize>
   </PropertyGroup>
 
 </Project>
diff --git a/Ryujinx.Profiler/DumpProfile.cs b/Ryujinx.Profiler/DumpProfile.cs
new file mode 100644
index 00000000..62a02761
--- /dev/null
+++ b/Ryujinx.Profiler/DumpProfile.cs
@@ -0,0 +1,35 @@
+using Ryujinx.Common;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Ryujinx.Profiler
+{
+    public static class DumpProfile
+    {
+        public static void ToFile(string path, InternalProfile profile)
+        {
+            String fileData = "Category,Session Group,Session Item,Count,Average(ms),Total(ms)\r\n";
+
+            foreach (KeyValuePair<ProfileConfig, TimingInfo> time in profile.Timers.OrderBy(key => key.Key.Tag))
+            {
+                fileData += $"{time.Key.Category}," +
+                            $"{time.Key.SessionGroup}," +
+                            $"{time.Key.SessionItem}," +
+                            $"{time.Value.Count}," +
+                            $"{time.Value.AverageTime / PerformanceCounter.TicksPerMillisecond}," +
+                            $"{time.Value.TotalTime / PerformanceCounter.TicksPerMillisecond}\r\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 00000000..bd522b00
--- /dev/null
+++ b/Ryujinx.Profiler/InternalProfile.cs
@@ -0,0 +1,220 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Ryujinx.Common;
+
+namespace Ryujinx.Profiler
+{
+    public class InternalProfile
+    {
+        private struct TimerQueueValue
+        {
+            public ProfileConfig Config;
+            public long Time;
+            public bool IsBegin;
+        }
+
+        internal Dictionary<ProfileConfig, TimingInfo> Timers { get; set; }
+
+        private readonly object _timerQueueClearLock = new object();
+        private ConcurrentQueue<TimerQueueValue> _timerQueue;
+
+        private int _sessionCounter = 0;
+
+        // Cleanup thread
+        private readonly Thread _cleanupThread;
+        private bool _cleanupRunning;
+        private readonly long _history;
+        private long _preserve;
+
+        // Timing flags
+        private TimingFlag[] _timingFlags;
+        private long[] _timingFlagAverages;
+        private long[] _timingFlagLast;
+        private long[] _timingFlagLastDelta;
+        private int _timingFlagCount;
+        private int _timingFlagIndex;
+
+        private int _maxFlags;
+
+        private Action<TimingFlag> _timingFlagCallback;
+
+        public InternalProfile(long history, int maxFlags)
+        {
+            _maxFlags            = maxFlags;
+            Timers               = new Dictionary<ProfileConfig, TimingInfo>();
+            _timingFlags         = new TimingFlag[_maxFlags];
+            _timingFlagAverages  = new long[(int)TimingFlagType.Count];
+            _timingFlagLast      = new long[(int)TimingFlagType.Count];
+            _timingFlagLastDelta = new long[(int)TimingFlagType.Count];
+            _timerQueue          = new ConcurrentQueue<TimerQueueValue>();
+            _history             = history;
+            _cleanupRunning      = true;
+
+            // Create cleanup thread.
+            _cleanupThread = new Thread(CleanupLoop);
+            _cleanupThread.Start();
+        }
+
+        private void CleanupLoop()
+        {
+            bool queueCleared = false;
+
+            while (_cleanupRunning)
+            {
+                // Ensure we only ever have 1 instance modifying timers or timerQueue
+                if (Monitor.TryEnter(_timerQueueClearLock))
+                {
+                    queueCleared = ClearTimerQueue();
+
+                    // Calculate before foreach to mitigate redundant calculations
+                    long cleanupBefore = PerformanceCounter.ElapsedTicks - _history;
+                    long preserveStart = _preserve - _history;
+
+                    // Each cleanup is self contained so run in parallel for maximum efficiency
+                    Parallel.ForEach(Timers, (t) => t.Value.Cleanup(cleanupBefore, preserveStart, _preserve));
+
+                    Monitor.Exit(_timerQueueClearLock);
+                }
+
+                // Only sleep if queue was sucessfully cleared
+                if (queueCleared)
+                {
+                    Thread.Sleep(5);
+                }
+            }
+        }
+
+        private bool ClearTimerQueue()
+        {
+            int count = 0;
+
+            while (_timerQueue.TryDequeue(out var item))
+            {
+                if (!Timers.TryGetValue(item.Config, out var value))
+                {
+                    value = new TimingInfo();
+                    Timers.Add(item.Config, value);
+                }
+
+                if (item.IsBegin)
+                {
+                    value.Begin(item.Time);
+                }
+                else
+                {
+                    value.End(item.Time);
+                }
+
+                // Don't block for too long as memory disposal is blocked while this function runs
+                if (count++ > 10000)
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        public void FlagTime(TimingFlagType flagType)
+        {
+            int flagId = (int)flagType;
+
+            _timingFlags[_timingFlagIndex] = new TimingFlag()
+            {
+                FlagType  = flagType,
+                Timestamp = PerformanceCounter.ElapsedTicks
+            };
+
+            _timingFlagCount = Math.Max(_timingFlagCount + 1, _maxFlags);
+
+            // Work out average
+            if (_timingFlagLast[flagId] != 0)
+            {
+                _timingFlagLastDelta[flagId] = _timingFlags[_timingFlagIndex].Timestamp - _timingFlagLast[flagId];
+                _timingFlagAverages[flagId]  = (_timingFlagAverages[flagId] == 0) ? _timingFlagLastDelta[flagId] :
+                                                                                   (_timingFlagLastDelta[flagId] + _timingFlagAverages[flagId]) >> 1;
+            }
+            _timingFlagLast[flagId] = _timingFlags[_timingFlagIndex].Timestamp;
+
+            // Notify subscribers
+            _timingFlagCallback?.Invoke(_timingFlags[_timingFlagIndex]);
+
+            if (++_timingFlagIndex >= _maxFlags)
+            {
+                _timingFlagIndex = 0;
+            }
+        }
+
+        public void BeginProfile(ProfileConfig config)
+        {
+            _timerQueue.Enqueue(new TimerQueueValue()
+            {
+                Config  = config,
+                IsBegin = true,
+                Time    = PerformanceCounter.ElapsedTicks,
+            });
+        }
+
+        public void EndProfile(ProfileConfig config)
+        {
+            _timerQueue.Enqueue(new TimerQueueValue()
+            {
+                Config  = config,
+                IsBegin = false,
+                Time    = PerformanceCounter.ElapsedTicks,
+            });
+        }
+
+        public string GetSession()
+        {
+            // Can be called from multiple threads so we need to ensure no duplicate sessions are generated
+            return Interlocked.Increment(ref _sessionCounter).ToString();
+        }
+
+        public List<KeyValuePair<ProfileConfig, TimingInfo>> GetProfilingData()
+        {
+            _preserve = PerformanceCounter.ElapsedTicks;
+
+            lock (_timerQueueClearLock)
+            {
+                ClearTimerQueue();
+                return Timers.ToList();
+            }
+        }
+
+        public TimingFlag[] GetTimingFlags()
+        {
+            int count = Math.Max(_timingFlagCount, _maxFlags);
+            TimingFlag[] outFlags = new TimingFlag[count];
+            
+            for (int i = 0, sourceIndex = _timingFlagIndex; i < count; i++, sourceIndex++)
+            {
+                if (sourceIndex >= _maxFlags)
+                    sourceIndex = 0;
+                outFlags[i] = _timingFlags[sourceIndex];
+            }
+
+            return outFlags;
+        }
+
+        public (long[], long[]) GetTimingAveragesAndLast()
+        {
+            return (_timingFlagAverages, _timingFlagLastDelta);
+        }
+
+        public void RegisterFlagReciever(Action<TimingFlag> reciever)
+        {
+            _timingFlagCallback = reciever;
+        }
+
+        public void Dispose()
+        {
+            _cleanupRunning = false;
+            _cleanupThread.Join();
+        }
+    }
+}
diff --git a/Ryujinx.Profiler/Profile.cs b/Ryujinx.Profiler/Profile.cs
new file mode 100644
index 00000000..fcd50c69
--- /dev/null
+++ b/Ryujinx.Profiler/Profile.cs
@@ -0,0 +1,143 @@
+using Ryujinx.Common;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Ryujinx.Profiler
+{
+    public static class Profile
+    {
+        public static float UpdateRate    => _settings.UpdateRate;
+        public static long  HistoryLength => _settings.History;
+
+        public static ProfilerKeyboardHandler Controls      => _settings.Controls;
+
+        private static InternalProfile  _profileInstance;
+        private static ProfilerSettings _settings;
+
+        [Conditional("USE_PROFILING")]
+        public static void Initalize()
+        {
+            var config = ProfilerConfiguration.Load("ProfilerConfig.jsonc");
+
+            _settings = new ProfilerSettings()
+            {
+                Enabled         = config.Enabled,
+                FileDumpEnabled = config.DumpPath != "",
+                DumpLocation    = config.DumpPath,
+                UpdateRate      = (config.UpdateRate <= 0) ? -1 : 1.0f / config.UpdateRate,
+                History         = (long)(config.History * PerformanceCounter.TicksPerSecond),
+                MaxLevel        = config.MaxLevel,
+                Controls        = config.Controls,
+                MaxFlags        = config.MaxFlags,
+            };
+        }
+
+        public static bool ProfilingEnabled()
+        {
+#if USE_PROFILING
+            if (!_settings.Enabled)
+                return false;
+
+            if (_profileInstance == null)
+                _profileInstance = new InternalProfile(_settings.History, _settings.MaxFlags);
+
+            return true;
+#else
+            return false;
+#endif
+        }
+
+        [Conditional("USE_PROFILING")]
+        public static void FinishProfiling()
+        {
+            if (!ProfilingEnabled())
+                return;
+
+            if (_settings.FileDumpEnabled)
+                DumpProfile.ToFile(_settings.DumpLocation, _profileInstance);
+
+            _profileInstance.Dispose();
+        }
+
+        [Conditional("USE_PROFILING")]
+        public static void FlagTime(TimingFlagType flagType)
+        {
+            if (!ProfilingEnabled())
+                return;
+            _profileInstance.FlagTime(flagType);
+        }
+
+        [Conditional("USE_PROFILING")]
+        public static void RegisterFlagReciever(Action<TimingFlag> reciever)
+        {
+            if (!ProfilingEnabled())
+                return;
+            _profileInstance.RegisterFlagReciever(reciever);
+        }
+
+        [Conditional("USE_PROFILING")]
+        public static void Begin(ProfileConfig config)
+        {
+            if (!ProfilingEnabled())
+                return;
+            if (config.Level > _settings.MaxLevel)
+                return;
+            _profileInstance.BeginProfile(config);
+        }
+
+        [Conditional("USE_PROFILING")]
+        public static void End(ProfileConfig config)
+        {
+            if (!ProfilingEnabled())
+                return;
+            if (config.Level > _settings.MaxLevel)
+                return;
+            _profileInstance.EndProfile(config);
+        }
+
+        public static string GetSession()
+        {
+#if USE_PROFILING
+            if (!ProfilingEnabled())
+                return null;
+            return _profileInstance.GetSession();
+#else
+            return "";
+#endif
+        }
+
+        public static List<KeyValuePair<ProfileConfig, TimingInfo>> GetProfilingData()
+        {
+#if USE_PROFILING
+            if (!ProfilingEnabled())
+                return new List<KeyValuePair<ProfileConfig, TimingInfo>>();
+            return _profileInstance.GetProfilingData();
+#else
+            return new List<KeyValuePair<ProfileConfig, TimingInfo>>();
+#endif
+        }
+
+        public static TimingFlag[] GetTimingFlags()
+        {
+#if USE_PROFILING
+            if (!ProfilingEnabled())
+                return new TimingFlag[0];
+            return _profileInstance.GetTimingFlags();
+#else
+            return new TimingFlag[0];
+#endif
+        }
+
+        public static (long[], long[]) GetTimingAveragesAndLast()
+        {
+#if USE_PROFILING
+            if (!ProfilingEnabled())
+                return (new long[0], new long[0]);
+            return _profileInstance.GetTimingAveragesAndLast();
+#else
+            return (new long[0], new long[0]);
+#endif
+        }
+    }
+}
diff --git a/Ryujinx.Profiler/ProfileConfig.cs b/Ryujinx.Profiler/ProfileConfig.cs
new file mode 100644
index 00000000..6a2b2bc0
--- /dev/null
+++ b/Ryujinx.Profiler/ProfileConfig.cs
@@ -0,0 +1,113 @@
+using System;
+
+namespace Ryujinx.Profiler
+{
+    public struct ProfileConfig : IEquatable<ProfileConfig>
+    {
+        public string Category;
+        public string SessionGroup;
+        public string SessionItem;
+
+        public int Level;
+
+        // Private cached variables
+        private string _cachedTag;
+        private string _cachedSession;
+        private string _cachedSearch;
+
+        // Public helpers to get config in more user friendly format,
+        // Cached because they never change and are called often
+        public string Search
+        {
+            get
+            {
+                if (_cachedSearch == null)
+                {
+                    _cachedSearch = $"{Category}.{SessionGroup}.{SessionItem}";
+                }
+
+                return _cachedSearch;
+            }
+        }
+
+        public string Tag
+        {
+            get
+            {
+                if (_cachedTag == null)
+                    _cachedTag = $"{Category}{(Session == "" ? "" : $" ({Session})")}";
+                return _cachedTag;
+            }
+        }
+
+        public string Session
+        {
+            get
+            {
+                if (_cachedSession == null)
+                {
+                    if (SessionGroup != null && SessionItem != null)
+                    {
+                        _cachedSession = $"{SessionGroup}: {SessionItem}";
+                    }
+                    else if (SessionGroup != null)
+                    {
+                        _cachedSession = $"{SessionGroup}";
+                    }
+                    else if (SessionItem != null)
+                    {
+                        _cachedSession = $"---: {SessionItem}";
+                    }
+                    else
+                    {
+                        _cachedSession = "";
+                    }
+                }
+
+                return _cachedSession;
+            }
+        }
+
+        /// <summary>
+        /// The default comparison is far too slow for the number of comparisons needed because it doesn't know what's important to compare
+        /// </summary>
+        /// <param name="obj">Object to compare to</param>
+        /// <returns></returns>
+        public bool Equals(ProfileConfig cmpObj)
+        {
+            // Order here is important.
+            // Multiple entries with the same item is considerable less likely that multiple items with the same group.
+            // Likewise for group and category.
+            return (cmpObj.SessionItem  == SessionItem && 
+                    cmpObj.SessionGroup == SessionGroup && 
+                    cmpObj.Category     == Category);
+        }
+    }
+
+    /// <summary>
+    /// Predefined configs to make profiling easier,
+    /// nested so you can reference as Profiles.Category.Group.Item where item and group may be optional
+    /// </summary>
+    public static class Profiles
+    {
+        public static class CPU
+        {
+            public static ProfileConfig TranslateTier0 = new ProfileConfig()
+            {
+                Category = "CPU",
+                SessionGroup = "TranslateTier0"
+            };
+
+            public static ProfileConfig TranslateTier1 = new ProfileConfig()
+            {
+                Category = "CPU",
+                SessionGroup = "TranslateTier1"
+            };
+        }
+
+        public static ProfileConfig ServiceCall = new ProfileConfig()
+        {
+            Category = "ServiceCall",
+        };
+    }
+}
diff --git a/Ryujinx.Profiler/ProfilerConfig.jsonc b/Ryujinx.Profiler/ProfilerConfig.jsonc
new file mode 100644
index 00000000..e6714386
--- /dev/null
+++ b/Ryujinx.Profiler/ProfilerConfig.jsonc
@@ -0,0 +1,28 @@
+{
+  // Enable profiling (Only available on a profiling enabled builds)
+  "enabled": true,
+
+  // Set profile file dump location, if blank file dumping disabled. (e.g. `ProfileDump.csv`)
+  "dump_path": "",
+
+  // Update rate for profiler UI, in hertz. -1 updates every time a frame is issued
+  "update_rate": 4.0,
+
+  // Set how long to keep profiling data in seconds, reduce if profiling is taking too much RAM
+  "history": 5.0,
+
+  // Set the maximum profiling level. Higher values may cause a heavy load on your system but will allow you to profile in more detail
+  "max_level": 0,
+
+  // Sets the maximum number of flags to keep
+  "max_flags": 1000, 
+
+  // Keyboard Controls
+  // https://github.com/opentk/opentk/blob/master/src/OpenTK/Input/Key.cs
+  "controls": {
+    "buttons": {
+      // Show/Hide the profiler
+      "toggle_profiler": "F2"
+    }
+  }
+}
\ No newline at end of file
diff --git a/Ryujinx.Profiler/ProfilerConfiguration.cs b/Ryujinx.Profiler/ProfilerConfiguration.cs
new file mode 100644
index 00000000..b4d629e4
--- /dev/null
+++ b/Ryujinx.Profiler/ProfilerConfiguration.cs
@@ -0,0 +1,73 @@
+using OpenTK.Input;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Utf8Json;
+using Utf8Json.Resolvers;
+
+namespace Ryujinx.Profiler
+{
+    public class ProfilerConfiguration
+    {
+        public bool   Enabled    { get; private set; }
+        public string DumpPath   { get; private set; }
+        public float  UpdateRate { get; private set; }
+        public int    MaxLevel   { get; private set; }
+        public int    MaxFlags   { get; private set; }
+        public float  History    { get; private set; }
+
+        public ProfilerKeyboardHandler Controls { get; private set; }
+
+        /// <summary>
+        /// Loads a configuration file from disk
+        /// </summary>
+        /// <param name="path">The path to the JSON configuration file</param>
+        public static ProfilerConfiguration Load(string path)
+        {
+            var resolver = CompositeResolver.Create(
+                new[] { new ConfigurationEnumFormatter<Key>() },
+                new[] { StandardResolver.AllowPrivateSnakeCase }
+            );
+
+            if (!File.Exists(path))
+            {
+                throw new FileNotFoundException($"Profiler configuration file {path} not found");
+            }
+
+            using (Stream stream = File.OpenRead(path))
+            {
+                return JsonSerializer.Deserialize<ProfilerConfiguration>(stream, resolver);
+            }
+        }
+
+        private class ConfigurationEnumFormatter<T> : IJsonFormatter<T>
+            where T : struct
+        {
+            public void Serialize(ref JsonWriter writer, T value, IJsonFormatterResolver formatterResolver)
+            {
+                formatterResolver.GetFormatterWithVerify<string>()
+                    .Serialize(ref writer, value.ToString(), formatterResolver);
+            }
+
+            public T Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
+            {
+                if (reader.ReadIsNull())
+                {
+                    return default(T);
+                }
+
+                var enumName = formatterResolver.GetFormatterWithVerify<string>()
+                    .Deserialize(ref reader, formatterResolver);
+
+                if (Enum.TryParse<T>(enumName, out T result))
+                {
+                    return result;
+                }
+
+                return default(T);
+            }
+        }
+    }
+}
diff --git a/Ryujinx.Profiler/ProfilerKeyboardHandler.cs b/Ryujinx.Profiler/ProfilerKeyboardHandler.cs
new file mode 100644
index 00000000..e1075c8d
--- /dev/null
+++ b/Ryujinx.Profiler/ProfilerKeyboardHandler.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using OpenTK.Input;
+
+namespace Ryujinx.Profiler
+{
+    public struct ProfilerButtons
+    {
+        public Key ToggleProfiler;
+    }
+
+    public class ProfilerKeyboardHandler
+    {
+        public ProfilerButtons Buttons;
+
+        private KeyboardState _prevKeyboard;
+
+        public ProfilerKeyboardHandler(ProfilerButtons buttons)
+        {
+            Buttons = buttons;
+        }
+
+        public bool TogglePressed(KeyboardState keyboard) => !keyboard[Buttons.ToggleProfiler] && _prevKeyboard[Buttons.ToggleProfiler];
+
+        public void SetPrevKeyboardState(KeyboardState keyboard)
+        {
+            _prevKeyboard = keyboard;
+        }
+    }
+}
diff --git a/Ryujinx.Profiler/Ryujinx.Profiler.csproj b/Ryujinx.Profiler/Ryujinx.Profiler.csproj
new file mode 100644
index 00000000..5a4c8f4f
--- /dev/null
+++ b/Ryujinx.Profiler/Ryujinx.Profiler.csproj
@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <Configurations>Debug;Release;Profile Debug;Profile Release</Configurations>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+    <DefineConstants>TRACE</DefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>false</Optimize>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="OpenTK.NetStandard" Version="1.0.4" />
+    <PackageReference Include="SharpFontCore" Version="0.1.1" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
+  </ItemGroup>
+
+  <ItemGroup Condition="'$(Configuration)' == 'Profile Debug' Or '$(Configuration)' == 'Profile Release'">
+    <None Update="ProfilerConfig.jsonc">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+</Project>
diff --git a/Ryujinx.Profiler/Settings.cs b/Ryujinx.Profiler/Settings.cs
new file mode 100644
index 00000000..c0393545
--- /dev/null
+++ b/Ryujinx.Profiler/Settings.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Ryujinx.Profiler
+{
+    public class ProfilerSettings
+    {
+        // Default settings for profiler
+        public bool   Enabled         { get; set; } = false;
+        public bool   FileDumpEnabled { get; set; } = false;
+        public string DumpLocation    { get; set; } = "";
+        public float  UpdateRate      { get; set; } = 0.1f;
+        public int    MaxLevel        { get; set; } = 0;
+        public int    MaxFlags        { get; set; } = 1000;
+
+        // 19531225 = 5 seconds in ticks on most pc's.
+        // It should get set on boot to the time specified in config
+        public long History { get; set; } = 19531225;
+
+        // Controls
+        public ProfilerKeyboardHandler Controls;
+    }
+}
diff --git a/Ryujinx.Profiler/TimingFlag.cs b/Ryujinx.Profiler/TimingFlag.cs
new file mode 100644
index 00000000..7d7c715f
--- /dev/null
+++ b/Ryujinx.Profiler/TimingFlag.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Text;
+
+namespace Ryujinx.Profiler
+{
+    public enum TimingFlagType
+    {
+        FrameSwap   = 0,
+        SystemFrame = 1,
+
+        // Update this for new flags
+        Count       = 2,
+    }
+
+    public struct TimingFlag
+    {
+        public TimingFlagType FlagType;
+        public long Timestamp;
+    }
+}
diff --git a/Ryujinx.Profiler/TimingInfo.cs b/Ryujinx.Profiler/TimingInfo.cs
new file mode 100644
index 00000000..e444e423
--- /dev/null
+++ b/Ryujinx.Profiler/TimingInfo.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.Profiler
+{
+    public struct Timestamp
+    {
+        public long BeginTime;
+        public long EndTime;
+    }
+
+    public class TimingInfo
+    {
+        // Timestamps
+        public long TotalTime { get; set; }
+        public long Instant   { get; set; }
+
+        // Measurement counts
+        public int  Count        { get; set; }
+        public int  InstantCount { get; set; }
+
+        // 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
+        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;
+
+        public TimingInfo()
+        {
+            _timestamps = new List<Timestamp>();
+            _depth      = 0;
+        }
+
+        public void Begin(long beginTime)
+        {
+            lock (_timestampLock)
+            {
+                // Finish current timestamp if already running
+                if (_depth > 0)
+                {
+                    EndUnsafe(beginTime);
+                }
+
+                BeginUnsafe(beginTime);
+                _depth++;
+            }
+        }
+
+        private void BeginUnsafe(long beginTime)
+        {
+            _currentTimestamp.BeginTime = beginTime;
+            _currentTimestamp.EndTime   = -1;
+        }
+
+        public void End(long endTime)
+        {
+            lock (_timestampLock)
+            {
+                _depth--;
+
+                if (_depth < 0)
+                {
+                    throw new Exception("Timing info end called without corresponding begin");
+                }
+
+                EndUnsafe(endTime);
+
+                // Still have others using this timing info so recreate start for them
+                if (_depth > 0)
+                {
+                    BeginUnsafe(endTime);
+                }
+            }
+        }
+
+        private void EndUnsafe(long endTime)
+        {
+            _currentTimestamp.EndTime = endTime;
+            lock (_timestampListLock)
+            {
+                _timestamps.Add(_currentTimestamp);
+            }
+
+            var delta  = _currentTimestamp.EndTime - _currentTimestamp.BeginTime;
+            TotalTime += delta;
+            Instant   += delta;
+
+            Count++;
+            InstantCount++;
+        }
+
+        // Remove any timestamps before given timestamp to free memory
+        public void Cleanup(long before, long preserveStart, long preserveEnd)
+        {
+            lock (_timestampListLock)
+            {
+                int toRemove        = 0;
+                int toPreserveStart = 0;
+                int toPreserveLen   = 0;
+
+                for (int i = 0; i < _timestamps.Count; i++)
+                {
+                    if (_timestamps[i].EndTime < preserveStart)
+                    {
+                        toPreserveStart++;
+                        InstantCount--;
+                        Instant -= _timestamps[i].EndTime - _timestamps[i].BeginTime;
+                    }
+                    else if (_timestamps[i].EndTime < preserveEnd)
+                    {
+                        toPreserveLen++;
+                    }
+                    else if (_timestamps[i].EndTime < before)
+                    {
+                        toRemove++;
+                        InstantCount--;
+                        Instant -= _timestamps[i].EndTime - _timestamps[i].BeginTime;
+                    }
+                    else
+                    {
+                        // Assume timestamps are in chronological order so no more need to be removed
+                        break;
+                    }
+                }
+
+                if (toPreserveStart > 0)
+                {
+                    _timestamps.RemoveRange(0, toPreserveStart);
+                }
+
+                if (toRemove > 0)
+                {
+                    _timestamps.RemoveRange(toPreserveLen, toRemove);
+                }
+            }
+        }
+
+        public Timestamp[] GetAllTimestamps()
+        {
+            lock (_timestampListLock)
+            {
+                Timestamp[] returnTimestamps = new Timestamp[_timestamps.Count];
+                _timestamps.CopyTo(returnTimestamps);
+                return returnTimestamps;
+            }
+        }
+    }
+}
diff --git a/Ryujinx.Profiler/UI/ProfileButton.cs b/Ryujinx.Profiler/UI/ProfileButton.cs
new file mode 100644
index 00000000..7e2ae728
--- /dev/null
+++ b/Ryujinx.Profiler/UI/ProfileButton.cs
@@ -0,0 +1,110 @@
+using System;
+using OpenTK;
+using OpenTK.Graphics.OpenGL;
+using Ryujinx.Profiler.UI.SharpFontHelpers;
+
+namespace Ryujinx.Profiler.UI
+{
+    public class ProfileButton
+    {
+        // Store font service
+        private FontService _fontService;
+
+        // Layout information
+        private int _left, _right;
+        private int _bottom, _top;
+        private int _height;
+        private int _padding;
+
+        // Label information
+        private int    _labelX, _labelY;
+        private string _label;
+
+        // Misc
+        private Action _clicked;
+        private bool   _visible;
+
+        public ProfileButton(FontService fontService, Action clicked)
+            : this(fontService, clicked, 0, 0, 0, 0, 0)
+        {
+            _visible = false;
+        }
+
+        public ProfileButton(FontService fontService, Action clicked, int x, int y, int padding, int height, int width)
+            : this(fontService, "", clicked, x, y, padding, height, width)
+        {
+            _visible = false;
+        }
+
+        public ProfileButton(FontService fontService, string label, Action clicked, int x, int y, int padding, int height, int width = -1)
+        {
+            _fontService = fontService;
+            _clicked     = clicked;
+
+            UpdateSize(label, x, y, padding, height, width);
+        }
+
+        public int UpdateSize(string label, int x, int y, int padding, int height, int width = -1)
+        {
+            _visible = true;
+            _label   = label;
+
+            if (width == -1)
+            {
+                // Dummy draw to measure size
+                width = (int)_fontService.DrawText(label, 0, 0, height, false);
+            }
+
+            UpdateSize(x, y, padding, width, height);
+
+            return _right - _left;
+        }
+
+        public void UpdateSize(int x, int y, int padding, int width, int height)
+        {
+            _height = height;
+            _left   = x;
+            _bottom = y;
+            _labelX = x + padding / 2;
+            _labelY = y + padding / 2;
+            _top    = y + height + padding;
+            _right  = x + width + padding;
+        }
+
+        public void Draw()
+        {
+            if (!_visible)
+            {
+                return;
+            }
+
+            // Draw backing rectangle
+            GL.Begin(PrimitiveType.Triangles);
+            GL.Color3(Color.Black);
+            GL.Vertex2(_left,  _bottom);
+            GL.Vertex2(_left,  _top);
+            GL.Vertex2(_right, _top);
+
+            GL.Vertex2(_right, _top);
+            GL.Vertex2(_right, _bottom);
+            GL.Vertex2(_left,  _bottom);
+            GL.End();
+
+            // Use font service to draw label
+            _fontService.DrawText(_label, _labelX, _labelY, _height);
+        }
+
+        public bool ProcessClick(int x, int y)
+        {
+            // If button contains x, y
+            if (x > _left   && x < _right &&
+                y > _bottom && y < _top)
+            {
+                _clicked();
+                return true;
+            }
+
+            return false;
+        }
+    }
+}
diff --git a/Ryujinx.Profiler/UI/ProfileSorters.cs b/Ryujinx.Profiler/UI/ProfileSorters.cs
new file mode 100644
index 00000000..2d06f426
--- /dev/null
+++ b/Ryujinx.Profiler/UI/ProfileSorters.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Ryujinx.Profiler.UI
+{
+    public static class ProfileSorters
+    {
+        public class InstantAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>>
+        {
+            public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2)
+                => pair2.Value.Instant.CompareTo(pair1.Value.Instant);
+        }
+
+        public class AverageAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>>
+        {
+            public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2)
+                => pair2.Value.AverageTime.CompareTo(pair1.Value.AverageTime);
+        }
+
+        public class TotalAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>>
+        {
+            public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2)
+                => pair2.Value.TotalTime.CompareTo(pair1.Value.TotalTime);
+        }
+
+        public class TagAscending : IComparer<KeyValuePair<ProfileConfig, TimingInfo>>
+        {
+            public int Compare(KeyValuePair<ProfileConfig, TimingInfo> pair1, KeyValuePair<ProfileConfig, TimingInfo> pair2)
+                => StringComparer.CurrentCulture.Compare(pair1.Key.Search, pair2.Key.Search);
+        }
+    }
+}
diff --git a/Ryujinx.Profiler/UI/ProfileWindow.cs b/Ryujinx.Profiler/UI/ProfileWindow.cs
new file mode 100644
index 00000000..c58b9235
--- /dev/null
+++ b/Ryujinx.Profiler/UI/ProfileWindow.cs
@@ -0,0 +1,773 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text.RegularExpressions;
+using OpenTK;
+using OpenTK.Graphics;
+using OpenTK.Graphics.OpenGL;
+using OpenTK.Input;
+using Ryujinx.Common;
+using Ryujinx.Profiler.UI.SharpFontHelpers;
+
+namespace Ryujinx.Profiler.UI
+{
+    public partial class ProfileWindow : GameWindow
+    {
+        // List all buttons for index in button array
+        private enum ButtonIndex
+        {
+            TagTitle          = 0,
+            InstantTitle      = 1,
+            AverageTitle      = 2,
+            TotalTitle        = 3,
+            FilterBar         = 4,
+            ShowHideInactive  = 5,
+            Pause             = 6,
+            ChangeDisplay     = 7,
+
+            // Don't automatically draw after here
+            ToggleFlags       = 8,
+            Step              = 9,
+
+            // Update this when new buttons are added.
+            // These are indexes to the enum list
+            Autodraw = 8,
+            Count    = 10,
+        }
+
+        // Font service
+        private FontService _fontService;
+
+        // UI variables
+        private ProfileButton[] _buttons;
+
+        private bool _initComplete    = false;
+        private bool _visible         = true;
+        private bool _visibleChanged  = true;
+        private bool _viewportUpdated = true;
+        private bool _redrawPending   = true;
+        private bool _displayGraph    = true;
+        private bool _displayFlags    = true;
+        private bool _showInactive    = true;
+        private bool _paused          = false;
+        private bool _doStep          = false;
+
+        // Layout
+        private const int LineHeight      = 16;
+        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<KeyValuePair<ProfileConfig, TimingInfo>> _unsortedProfileData;
+        private IComparer<KeyValuePair<ProfileConfig, TimingInfo>> _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;
+        private float _minScroll = 0;
+        private float _maxScroll = 0;
+
+        // Profile data storage
+        private List<KeyValuePair<ProfileConfig, TimingInfo>> _sortedProfileData;
+        private long _captureTime;
+
+        // Input
+        private bool _backspaceDown       = false;
+        private bool _prevBackspaceDown   = false;
+        private double _backspaceDownTime = 0;
+
+        // F35 used as no key
+        private Key _graphControlKey = Key.F35;
+
+        // Event management
+        private double _updateTimer;
+        private double _processEventTimer;
+        private bool   _profileUpdated           = false;
+        private readonly object _profileDataLock = new object();
+
+        public ProfileWindow()
+                               // Graphigs mode enables 2xAA
+            : base(1280, 720, new GraphicsMode(new ColorFormat(8, 8, 8, 8), 1, 1, 2))
+        {
+            Title    = "Profiler";
+            Location = new Point(DisplayDevice.Default.Width  - 1280,
+                                (DisplayDevice.Default.Height - 720) - 50);
+
+            if (Profile.UpdateRate <= 0)
+            {
+                // Perform step regardless of flag type
+                Profile.RegisterFlagReciever((t) =>
+                {
+                    if (!_paused)
+                    {
+                        _doStep = true;
+                    }
+                });
+            }
+
+            // Large number to force an update on first update
+            _updateTimer = 0xFFFF;
+
+            Init();
+
+            // Release context for render thread
+            Context.MakeCurrent(null);
+        }
+        
+        public void ToggleVisible()
+        {
+            _visible = !_visible;
+            _visibleChanged = true;
+        }
+
+        private void SetSort(IComparer<KeyValuePair<ProfileConfig, TimingInfo>> filter)
+        {
+            _sortAction = filter;
+            _profileUpdated = true;
+        }
+
+#region OnLoad
+        /// <summary>
+        /// Setup OpenGL and load resources
+        /// </summary>
+        public void Init()
+        {
+            GL.ClearColor(Color.Black);
+            _fontService = new FontService();
+            _fontService.InitalizeTextures();
+            _fontService.UpdateScreenHeight(Height);
+
+            _buttons = new ProfileButton[(int)ButtonIndex.Count];
+            _buttons[(int)ButtonIndex.TagTitle]      = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.TagAscending()));
+            _buttons[(int)ButtonIndex.InstantTitle]  = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.InstantAscending()));
+            _buttons[(int)ButtonIndex.AverageTitle]  = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.AverageAscending()));
+            _buttons[(int)ButtonIndex.TotalTitle]    = new ProfileButton(_fontService, () => SetSort(new ProfileSorters.TotalAscending()));
+            _buttons[(int)ButtonIndex.Step]          = new ProfileButton(_fontService, () => _doStep = true);
+            _buttons[(int)ButtonIndex.FilterBar]     = new ProfileButton(_fontService, () =>
+            {
+                _profileUpdated = true;
+                _regexEnabled = !_regexEnabled;
+            });
+
+            _buttons[(int)ButtonIndex.ShowHideInactive] = new ProfileButton(_fontService, () =>
+            {
+                _profileUpdated = true;
+                _showInactive = !_showInactive;
+            });
+
+            _buttons[(int)ButtonIndex.Pause] = new ProfileButton(_fontService, () =>
+            {
+                _profileUpdated = true;
+                _paused = !_paused;
+            });
+
+            _buttons[(int)ButtonIndex.ToggleFlags] = new ProfileButton(_fontService, () =>
+            {
+                _displayFlags = !_displayFlags;
+                _redrawPending = true;
+            });
+
+            _buttons[(int)ButtonIndex.ChangeDisplay] = new ProfileButton(_fontService, () =>
+            {
+                _displayGraph = !_displayGraph;
+                _redrawPending = true;
+            });
+
+            Visible = _visible;
+        }
+#endregion
+
+#region OnResize
+        /// <summary>
+        /// Respond to resize events
+        /// </summary>
+        /// <param name="e">Contains information on the new GameWindow size.</param>
+        /// <remarks>There is no need to call the base implementation.</remarks>
+        protected override void OnResize(EventArgs e)
+        {
+            _viewportUpdated = true;
+        }
+#endregion
+
+#region OnClose
+        /// <summary>
+        /// Intercept close event and hide instead
+        /// </summary>
+        protected override void OnClosing(CancelEventArgs e)
+        {
+            // Hide window
+            _visible        = false;
+            _visibleChanged = true;
+
+            // Cancel close
+            e.Cancel = true;
+
+            base.OnClosing(e);
+        }
+#endregion
+
+#region OnUpdateFrame
+        /// <summary>
+        /// Profile Update Loop
+        /// </summary>
+        /// <param name="e">Contains timing information.</param>
+        /// <remarks>There is no need to call the base implementation.</remarks>
+        public void Update(FrameEventArgs e)
+        {
+            if (_visibleChanged)
+            {
+                Visible = _visible;
+                _visibleChanged = false;
+            }
+
+            // Backspace handling
+            if (_backspaceDown)
+            {
+                if (!_prevBackspaceDown)
+                {
+                    _backspaceDownTime = 0;
+                    FilterBackspace();
+                }
+                else
+                {
+                    _backspaceDownTime += e.Time;
+                    if (_backspaceDownTime > 0.3)
+                    {
+                        _backspaceDownTime -= 0.05;
+                        FilterBackspace();
+                    }
+                }
+            }
+            _prevBackspaceDown = _backspaceDown;
+
+            // Get timing data if enough time has passed
+            _updateTimer += e.Time;
+            if (_doStep || ((Profile.UpdateRate > 0) && (!_paused && (_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 ? _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 argException)
+                        {
+                            // 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;
+            }
+
+            // Check for events 20 times a second
+            _processEventTimer += e.Time;
+            if (_processEventTimer > 0.05)
+            {
+                ProcessEvents();
+
+                if (_graphControlKey != Key.F35)
+                {
+                    switch (_graphControlKey)
+                    {
+                        case Key.Left:
+                            _graphPosition += (long) (GraphMoveSpeed * e.Time);
+                            break;
+
+                        case Key.Right:
+                            _graphPosition = Math.Max(_graphPosition - (long) (GraphMoveSpeed * e.Time), 0);
+                            break;
+
+                        case Key.Up:
+                            _graphZoom = MathF.Min(_graphZoom + (float) (GraphZoomSpeed * e.Time), 100.0f);
+                            break;
+
+                        case Key.Down:
+                            _graphZoom = MathF.Max(_graphZoom - (float) (GraphZoomSpeed * e.Time), 1f);
+                            break;
+                    }
+
+                    _redrawPending = true;
+                }
+
+                _processEventTimer = 0;
+            }
+        }
+#endregion
+
+#region OnRenderFrame
+        /// <summary>
+        /// Profile Render Loop
+        /// </summary>
+        /// <remarks>There is no need to call the base implementation.</remarks>
+        public void Draw()
+        {
+            if (!_visible || !_initComplete)
+            {
+                return;
+            }
+            
+            // Update viewport
+            if (_viewportUpdated)
+            {
+                GL.Viewport(0, 0, Width, Height);
+
+                GL.MatrixMode(MatrixMode.Projection);
+                GL.LoadIdentity();
+                GL.Ortho(0, Width, 0, Height, 0.0, 4.0);
+
+                _fontService.UpdateScreenHeight(Height);
+
+                _viewportUpdated = false;
+                _redrawPending   = true;
+            }
+
+            if (!_redrawPending)
+            {
+                return;
+            }
+
+            // Frame setup
+            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
+            GL.ClearColor(Color.Black);
+
+            _fontService.fontColor = Color.White;
+            int verticalIndex   = 0;
+
+            float width;
+            float maxWidth = 0;
+            float yOffset  = _scrollPos - TitleHeight;
+            float xOffset  = 10;
+            float timingDataLeft;
+            float timingWidth;
+
+            // Background lines to make reading easier
+            #region Background Lines
+            GL.Enable(EnableCap.ScissorTest);
+            GL.Scissor(0, BottomBarHeight, Width, Height - TitleHeight - BottomBarHeight);
+            GL.Begin(PrimitiveType.Triangles);
+            GL.Color3(0.2f, 0.2f, 0.2f);
+            for (int i = 0; i < _sortedProfileData.Count; i += 2)
+            {
+                float top    = GetLineY(yOffset, LineHeight, LinePadding, false, i - 1);
+                float bottom = GetLineY(yOffset, LineHeight, LinePadding, false, i);
+
+                // Skip rendering out of bounds bars
+                if (top < 0 || bottom > Height)
+                    continue;
+
+                GL.Vertex2(0, bottom);
+                GL.Vertex2(0, top);
+                GL.Vertex2(Width, top);
+
+                GL.Vertex2(Width, top);
+                GL.Vertex2(Width, bottom);
+                GL.Vertex2(0, bottom);
+            }
+            GL.End();
+            _maxScroll = (LineHeight + LinePadding) * (_sortedProfileData.Count - 1);
+#endregion
+            
+            lock (_profileDataLock)
+            {
+// Display category
+#region Category
+                verticalIndex = 0;
+                foreach (var entry in _sortedProfileData)
+                {
+                    if (entry.Key.Category == null)
+                    {
+                        verticalIndex++;
+                        continue;
+                    }
+
+                    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);
+
+                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)
+                {
+                    if (entry.Key.SessionGroup == null)
+                    {
+                        verticalIndex++;
+                        continue;
+                    }
+
+                    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);
+
+                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)
+                {
+                    if (entry.Key.SessionItem == null)
+                    {
+                        verticalIndex++;
+                        continue;
+                    }
+
+                    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, BottomBarHeight, (int)timingWidth, Height - TitleHeight - BottomBarHeight);
+
+                if (_displayGraph)
+                {
+                    DrawGraph(xOffset, yOffset, timingWidth);
+                }
+                else
+                {
+                    DrawBars(xOffset, yOffset, timingWidth);
+                }
+
+                GL.Scissor(0, BottomBarHeight, Width, Height - TitleHeight - BottomBarHeight);
+
+                if (!_displayGraph)
+                {
+                    _fontService.DrawText("Blue: Instant,  Green: Avg,  Red: Total", xOffset, Height - TitleFontHeight, TitleFontHeight);
+                }
+
+                xOffset = Width - 360;
+
+// Display timestamps
+#region Timestamps
+                verticalIndex     = 0;
+                long totalInstant = 0;
+                long totalAverage = 0;
+                long totalTime    = 0;
+                long totalCount   = 0;
+
+                GL.Enable(EnableCap.ScissorTest);
+                foreach (var entry in _sortedProfileData)
+                {
+                    float y = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++);
+
+                    _fontService.DrawText($"{GetTimeString(entry.Value.Instant)} ({entry.Value.InstantCount})", xOffset, y, LineHeight);
+
+                    _fontService.DrawText(GetTimeString(entry.Value.AverageTime), 150 + xOffset, y, LineHeight);
+
+                    _fontService.DrawText(GetTimeString(entry.Value.TotalTime), 260 + xOffset, y, LineHeight);
+
+                    totalInstant += entry.Value.Instant;
+                    totalAverage += entry.Value.AverageTime;
+                    totalTime    += entry.Value.TotalTime;
+                    totalCount   += entry.Value.InstantCount;
+                }
+                GL.Disable(EnableCap.ScissorTest);
+
+                float yHeight = Height - TitleFontHeight;
+
+                _fontService.DrawText("Instant (Count)", xOffset, yHeight, TitleFontHeight);
+                _buttons[(int)ButtonIndex.InstantTitle].UpdateSize((int)xOffset, (int)yHeight, 0, 130, TitleFontHeight);
+
+                _fontService.DrawText("Average", 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);
+
+                // Totals
+                yHeight = FilterHeight + 3;
+                int textHeight = LineHeight - 2;
+
+                _fontService.fontColor = new Color(100, 100, 255, 255);
+                float tempWidth = _fontService.DrawText($"Host {GetTimeString(_timingFlagsLast[(int)TimingFlagType.SystemFrame])} " +
+                                                            $"({GetTimeString(_timingFlagsAverages[(int)TimingFlagType.SystemFrame])})", 5, yHeight, textHeight);
+
+                _fontService.fontColor = Color.Red;
+                _fontService.DrawText($"Game {GetTimeString(_timingFlagsLast[(int)TimingFlagType.FrameSwap])} " +
+                                          $"({GetTimeString(_timingFlagsAverages[(int)TimingFlagType.FrameSwap])})", 15 + tempWidth, yHeight, textHeight);
+                _fontService.fontColor = Color.White;
+                
+
+                _fontService.DrawText($"{GetTimeString(totalInstant)} ({totalCount})", xOffset,       yHeight, textHeight);
+                _fontService.DrawText(GetTimeString(totalAverage),                     150 + xOffset, yHeight, textHeight);
+                _fontService.DrawText(GetTimeString(totalTime),                        260 + xOffset, yHeight, textHeight);
+#endregion
+            }
+
+#region Bottom bar
+            // Show/Hide Inactive
+            float widthShowHideButton = _buttons[(int)ButtonIndex.ShowHideInactive].UpdateSize($"{(_showInactive ? "Hide" : "Show")} Inactive", 5, 5, 4, 16);
+
+            // Play/Pause
+            float widthPlayPauseButton = _buttons[(int)ButtonIndex.Pause].UpdateSize(_paused ? "Play" : "Pause", 15 + (int)widthShowHideButton, 5, 4, 16) + widthShowHideButton;
+
+            // Step
+            float widthStepButton = widthPlayPauseButton;
+
+            if (_paused)
+            {
+                widthStepButton += _buttons[(int)ButtonIndex.Step].UpdateSize("Step", (int)(25 + widthPlayPauseButton), 5, 4, 16) + 10;
+                _buttons[(int)ButtonIndex.Step].Draw();
+            }
+
+            // Change display
+            float widthChangeDisplay = _buttons[(int)ButtonIndex.ChangeDisplay].UpdateSize($"View: {(_displayGraph ? "Graph" : "Bars")}", 25 + (int)widthStepButton, 5, 4, 16) + widthStepButton;
+
+            width = widthChangeDisplay;
+
+            if (_displayGraph)
+            {
+                width += _buttons[(int) ButtonIndex.ToggleFlags].UpdateSize($"{(_displayFlags ? "Hide" : "Show")} Flags", 35 + (int)widthChangeDisplay, 5, 4, 16) + 10;
+                _buttons[(int)ButtonIndex.ToggleFlags].Draw();
+            }
+
+            // Filter bar
+            _fontService.DrawText($"{(_regexEnabled ? "Regex " : "Filter")}: {_filterText}", 35 + width, 7, 16);
+            _buttons[(int)ButtonIndex.FilterBar].UpdateSize((int)(45 + width), 0, 0, Width, FilterHeight);
+#endregion
+
+            // Draw buttons
+            for (int i = 0; i < (int)ButtonIndex.Autodraw; i++)
+            {
+                _buttons[i].Draw();
+            }
+            
+// Dividing lines
+#region Dividing lines
+            GL.Color3(Color.White);
+            GL.Begin(PrimitiveType.Lines);
+            // Top divider
+            GL.Vertex2(0, Height -TitleHeight);
+            GL.Vertex2(Width, Height - TitleHeight);
+
+            // Bottom divider
+            GL.Vertex2(0,     FilterHeight);
+            GL.Vertex2(Width, FilterHeight);
+
+            GL.Vertex2(0,     BottomBarHeight);
+            GL.Vertex2(Width, BottomBarHeight);
+
+            // Bottom vertical dividers
+            GL.Vertex2(widthShowHideButton + 10, 0);
+            GL.Vertex2(widthShowHideButton + 10, FilterHeight);
+
+            GL.Vertex2(widthPlayPauseButton + 20, 0);
+            GL.Vertex2(widthPlayPauseButton + 20, FilterHeight);
+
+            if (_paused)
+            {
+                GL.Vertex2(widthStepButton + 20, 0);
+                GL.Vertex2(widthStepButton + 20, FilterHeight);
+            }
+
+            if (_displayGraph)
+            {
+                GL.Vertex2(widthChangeDisplay + 30, 0);
+                GL.Vertex2(widthChangeDisplay + 30, FilterHeight);
+            }
+
+            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
+
+            _redrawPending = false;
+            SwapBuffers();
+        }
+#endregion
+
+        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 Height + offset - lineHeight - padding - ((lineHeight + padding) * line) + ((centre) ? padding : 0);
+        }
+
+        protected override void OnKeyPress(KeyPressEventArgs e)
+        {
+            _filterText += e.KeyChar;
+            _profileUpdated = true;
+        }
+
+        protected override void OnKeyDown(KeyboardKeyEventArgs e)
+        {
+            switch (e.Key)
+            {
+                case Key.BackSpace:
+                    _profileUpdated = _backspaceDown = true;
+                    return;
+
+                case Key.Left:
+                case Key.Right:
+                case Key.Up:
+                case Key.Down:
+                    _graphControlKey = e.Key;
+                    return;
+        }
+            base.OnKeyUp(e);
+        }
+
+        protected override void OnKeyUp(KeyboardKeyEventArgs e)
+        {
+            // Can't go into switch as value isn't constant
+            if (e.Key == Profile.Controls.Buttons.ToggleProfiler)
+            {
+                ToggleVisible();
+                return;
+            }
+
+            switch (e.Key)
+            {
+                case Key.BackSpace:
+                    _backspaceDown = false;
+                    return;
+
+                case Key.Left:
+                case Key.Right:
+                case Key.Up:
+                case Key.Down:
+                    _graphControlKey = Key.F35;
+                    return;
+            }
+            base.OnKeyUp(e);
+        }
+
+        protected override void OnMouseUp(MouseButtonEventArgs e)
+        {
+            foreach (ProfileButton button in _buttons)
+            {
+                if (button.ProcessClick(e.X, Height - e.Y))
+                    return;
+            }
+        }
+
+        protected override void OnMouseWheel(MouseWheelEventArgs e)
+        {
+            _scrollPos += e.Delta * -30;
+            if (_scrollPos < _minScroll)
+                _scrollPos = _minScroll;
+            if (_scrollPos > _maxScroll)
+                _scrollPos = _maxScroll;
+
+            _redrawPending = true;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Profiler/UI/ProfileWindowBars.cs b/Ryujinx.Profiler/UI/ProfileWindowBars.cs
new file mode 100644
index 00000000..b1955a07
--- /dev/null
+++ b/Ryujinx.Profiler/UI/ProfileWindowBars.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using OpenTK;
+using OpenTK.Graphics.OpenGL;
+
+namespace Ryujinx.Profiler.UI
+{
+    public partial class ProfileWindow
+    {
+        private void DrawBars(float xOffset, float yOffset, float width)
+        {
+            if (_sortedProfileData.Count != 0)
+            {
+                long maxAverage;
+                long maxTotal;
+
+                int verticalIndex = 0;
+                float barHeight   = (LineHeight - LinePadding) / 3.0f;
+
+                // 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);
+                }
+
+                GL.Enable(EnableCap.ScissorTest);
+                GL.Begin(PrimitiveType.Triangles);
+                foreach (var entry in _sortedProfileData)
+                {
+                    // Instant
+                    GL.Color3(Color.Blue);
+                    float bottom = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex++);
+                    float top    = bottom + barHeight;
+                    float right  = (float)entry.Value.Instant / maxInstant * width + xOffset;
+
+                    // Skip rendering out of bounds bars
+                    if (top < 0 || bottom > Height)
+                        continue;
+
+                    GL.Vertex2(xOffset, bottom);
+                    GL.Vertex2(xOffset, top);
+                    GL.Vertex2(right,   top);
+
+                    GL.Vertex2(right,   top);
+                    GL.Vertex2(right,   bottom);
+                    GL.Vertex2(xOffset, bottom);
+
+                    // Average
+                    GL.Color3(Color.Green);
+                    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(xOffset, bottom);
+
+                    // Total
+                    GL.Color3(Color.Red);
+                    top    += barHeight;
+                    bottom += barHeight;
+                    right   = (float)entry.Value.TotalTime / maxTotal * width + xOffset;
+
+                    GL.Vertex2(xOffset, bottom);
+                    GL.Vertex2(xOffset, top);
+                    GL.Vertex2(right, top);
+
+                    GL.Vertex2(right, top);
+                    GL.Vertex2(right, bottom);
+                    GL.Vertex2(xOffset, bottom);
+                }
+
+                GL.End();
+                GL.Disable(EnableCap.ScissorTest);
+            }
+        }
+    }
+}
diff --git a/Ryujinx.Profiler/UI/ProfileWindowGraph.cs b/Ryujinx.Profiler/UI/ProfileWindowGraph.cs
new file mode 100644
index 00000000..9d34be97
--- /dev/null
+++ b/Ryujinx.Profiler/UI/ProfileWindowGraph.cs
@@ -0,0 +1,151 @@
+using System;
+using OpenTK;
+using OpenTK.Graphics.OpenGL;
+using Ryujinx.Common;
+
+namespace Ryujinx.Profiler.UI
+{
+    public partial class ProfileWindow
+    {
+        // Colour index equal to timing flag type as int
+        private Color[] _timingFlagColours = new[]
+        {
+            new Color(150, 25, 25, 50), // FrameSwap   = 0
+            new Color(25, 25, 150, 50), // SystemFrame = 1
+        };
+
+        private TimingFlag[] _timingFlags;
+
+        private const float GraphMoveSpeed = 40000;
+        private const float GraphZoomSpeed = 50;
+
+        private float _graphZoom      = 1;
+        private float _graphPosition  = 0;
+
+        private void DrawGraph(float xOffset, float yOffset, float width)
+        {
+            if (_sortedProfileData.Count != 0)
+            {
+                int   left, right;
+                float top, bottom;
+
+                int    verticalIndex      = 0;
+                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;
+
+                GL.Enable(EnableCap.ScissorTest);
+
+                // Draw timing flags
+                if (_displayFlags)
+                {
+                    TimingFlagType prevType = TimingFlagType.Count;
+
+                    GL.Enable(EnableCap.Blend);
+                    GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
+
+                    GL.Begin(PrimitiveType.Lines);
+                    foreach (TimingFlag timingFlag in _timingFlags)
+                    {
+                        if (prevType != timingFlag.FlagType)
+                        {
+                            prevType = timingFlag.FlagType;
+                            GL.Color4(_timingFlagColours[(int)prevType]);
+                        }
+
+                        int x = (int)(graphRight - ((graphPositionTicks - timingFlag.Timestamp) / timeWidthTicks) * width);
+                        GL.Vertex2(x, 0);
+                        GL.Vertex2(x, Height);
+                    }
+                    GL.End();
+                    GL.Disable(EnableCap.Blend);
+                }
+
+                // Draw bars
+                GL.Begin(PrimitiveType.Triangles);
+                foreach (var entry in _sortedProfileData)
+                {
+                    long furthest = 0;
+
+                    bottom = GetLineY(yOffset, LineHeight, LinePadding, true, verticalIndex);
+                    top    = bottom + barHeight;
+
+                    // Skip rendering out of bounds bars
+                    if (top < 0 || bottom > Height)
+                    {
+                        verticalIndex++;
+                        continue;
+                    }
+
+
+                    GL.Color3(Color.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);
+
+                        // Make sure width is at least 1px
+                        right = Math.Max(left + 1, right);
+
+                        GL.Vertex2(left,  bottom);
+                        GL.Vertex2(left,  top);
+                        GL.Vertex2(right, top);
+
+                        GL.Vertex2(right, top);
+                        GL.Vertex2(right, bottom);
+                        GL.Vertex2(left,  bottom);
+                    }
+
+                    // Currently capturing timestamp
+                    GL.Color3(Color.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);
+
+                        GL.Vertex2(left,       bottom);
+                        GL.Vertex2(left,       top);
+                        GL.Vertex2(graphRight, top);
+
+                        GL.Vertex2(graphRight, top);
+                        GL.Vertex2(graphRight, bottom);
+                        GL.Vertex2(left,       bottom);
+                    }
+
+                    verticalIndex++;
+                }
+
+                GL.End();
+                GL.Disable(EnableCap.ScissorTest);
+
+                string label = $"-{MathF.Round(_graphPosition, 2)} ms";
+
+                // Dummy draw for measure
+                float labelWidth = _fontService.DrawText(label, 0, 0, LineHeight, false);
+                _fontService.DrawText(label, graphRight - labelWidth - LinePadding, FilterHeight + LinePadding, LineHeight);
+                
+                _fontService.DrawText($"-{MathF.Round((float)((timeWidthTicks / PerformanceCounter.TicksPerMillisecond) + _graphPosition), 2)} ms", xOffset + LinePadding, FilterHeight + LinePadding, LineHeight);
+            }
+        }
+    }
+}
diff --git a/Ryujinx.Profiler/UI/ProfileWindowManager.cs b/Ryujinx.Profiler/UI/ProfileWindowManager.cs
new file mode 100644
index 00000000..4ba0c881
--- /dev/null
+++ b/Ryujinx.Profiler/UI/ProfileWindowManager.cs
@@ -0,0 +1,90 @@
+using System.Diagnostics;
+using System.Threading;
+using OpenTK;
+using OpenTK.Input;
+using Ryujinx.Common;
+
+namespace Ryujinx.Profiler.UI
+{
+    public class ProfileWindowManager
+    {
+        private ProfileWindow _window;
+        private Thread _profileThread;
+        private Thread _renderThread;
+        private bool _profilerRunning;
+
+        // Timing
+        private double _prevTime;
+
+        public ProfileWindowManager()
+        {
+            if (Profile.ProfilingEnabled())
+            {
+                _profilerRunning = true;
+                _prevTime        = 0;
+                _profileThread   = new Thread(ProfileLoop);
+                _profileThread.Start();
+            }
+        }
+
+        public void ToggleVisible()
+        {
+            if (Profile.ProfilingEnabled())
+            {
+                _window.ToggleVisible();
+            }
+        }
+
+        public void Close()
+        {
+            if (_window != null)
+            {
+                _profilerRunning = false;
+                _window.Close();
+                _window.Dispose();
+            }
+
+            _window = null;
+        }
+
+        public void UpdateKeyInput(KeyboardState keyboard)
+        {
+            if (Profile.Controls.TogglePressed(keyboard))
+            {
+                ToggleVisible();
+            }
+            Profile.Controls.SetPrevKeyboardState(keyboard);
+        }
+
+        private void ProfileLoop()
+        {
+            using (_window = new ProfileWindow())
+            {
+                // Create thread for render loop
+                _renderThread = new Thread(RenderLoop);
+                _renderThread.Start();
+
+                while (_profilerRunning)
+                {
+                    double time = (double)PerformanceCounter.ElapsedTicks / PerformanceCounter.TicksPerSecond;
+                    _window.Update(new FrameEventArgs(time - _prevTime));
+                    _prevTime = time;
+
+                    // Sleep to be less taxing, update usually does very little
+                    Thread.Sleep(1);
+                }
+            }
+        }
+
+        private void RenderLoop()
+        {
+            _window.Context.MakeCurrent(_window.WindowInfo);
+
+            while (_profilerRunning)
+            {
+                _window.Draw();
+                Thread.Sleep(1);
+            }
+        }
+    }
+}
diff --git a/Ryujinx.Profiler/UI/SharpFontHelpers/FontService.cs b/Ryujinx.Profiler/UI/SharpFontHelpers/FontService.cs
new file mode 100644
index 00000000..e64c9da3
--- /dev/null
+++ b/Ryujinx.Profiler/UI/SharpFontHelpers/FontService.cs
@@ -0,0 +1,257 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using OpenTK;
+using OpenTK.Graphics.OpenGL;
+using SharpFont;
+
+namespace Ryujinx.Profiler.UI.SharpFontHelpers
+{
+    public class FontService
+    {
+        private struct CharacterInfo
+        {
+            public float Left;
+            public float Right;
+            public float Top;
+            public float Bottom;
+
+            public int Width;
+            public float Height;
+
+            public float AspectRatio;
+
+            public float BearingX;
+            public float BearingY;
+            public float Advance;
+        }
+
+        private const int SheetWidth  = 1024;
+        private const int SheetHeight = 512;
+        private int ScreenWidth, ScreenHeight;
+        private int CharacterTextureSheet;
+        private CharacterInfo[] characters;
+
+        public Color fontColor { get; set; } = Color.Black;
+
+        private string GetFontPath()
+        {
+            string fontFolder = System.Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
+
+            // Only uses Arial, add more fonts here if wanted
+            string path = Path.Combine(fontFolder, "arial.ttf");
+            if (File.Exists(path))
+            {
+                return path;
+            }
+
+            throw new Exception($"Profiler exception. Required font Courier New or Arial not installed to {fontFolder}");
+        }
+
+        public void InitalizeTextures()
+        {
+            // Create and init some vars
+            uint[] rawCharacterSheet = new uint[SheetWidth * SheetHeight];
+            int x;
+            int y;
+            int lineOffset;
+            int maxHeight;
+
+            x = y = lineOffset = maxHeight = 0;
+            characters = new CharacterInfo[94];
+
+            // Get font
+            var font = new FontFace(File.OpenRead(GetFontPath()));
+
+            // Update raw data for each character
+            for (int i = 0; i < 94; i++)
+            {
+                var surface = RenderSurface((char)(i + 33), font, out var xBearing, out var yBearing, out var advance);
+
+                characters[i] = UpdateTexture(surface, ref rawCharacterSheet, ref x, ref y, ref lineOffset);
+                characters[i].BearingX = xBearing;
+                characters[i].BearingY = yBearing;
+                characters[i].Advance  = advance;
+
+                if (maxHeight < characters[i].Height)
+                    maxHeight = (int)characters[i].Height;
+            }
+
+            // Fix height for characters shorter than line height
+            for (int i = 0; i < 94; i++)
+            {
+                characters[i].BearingX   /= characters[i].Width;
+                characters[i].BearingY   /= maxHeight;
+                characters[i].Advance    /= characters[i].Width;
+                characters[i].Height     /= maxHeight;
+                characters[i].AspectRatio = (float)characters[i].Width / maxHeight;
+            }
+
+            // Convert raw data into texture
+            CharacterTextureSheet = GL.GenTexture();
+            GL.BindTexture(TextureTarget.Texture2D, CharacterTextureSheet);
+
+            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
+            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
+            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS,     (int)TextureWrapMode.Clamp);
+            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,     (int)TextureWrapMode.Clamp);
+            
+            GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, SheetWidth, SheetHeight, 0, PixelFormat.Rgba, PixelType.UnsignedInt8888, rawCharacterSheet);
+
+            GL.BindTexture(TextureTarget.Texture2D, 0);
+        }
+
+        public void UpdateScreenHeight(int height)
+        {
+            ScreenHeight = height;
+        }
+
+        public float DrawText(string text, float x, float y, float height, bool draw = true)
+        {
+            float originalX = x;
+
+            // Skip out of bounds draw
+            if (y < height * -2 || y > ScreenHeight + height * 2)
+            {
+                draw = false;
+            }
+
+            if (draw)
+            {
+                // Use font map texture
+                GL.BindTexture(TextureTarget.Texture2D, CharacterTextureSheet);
+
+                // Enable blending and textures
+                GL.Enable(EnableCap.Texture2D);
+                GL.Enable(EnableCap.Blend);
+                GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
+
+                // Draw all characters
+                GL.Begin(PrimitiveType.Triangles);
+                GL.Color4(fontColor);
+            }
+
+            for (int i = 0; i < text.Length; i++)
+            {
+                if (text[i] == ' ')
+                {
+                    x += height / 4;
+                    continue;
+                }
+
+                CharacterInfo charInfo = characters[text[i] - 33];
+                float width = (charInfo.AspectRatio * height);
+                x += (charInfo.BearingX * charInfo.AspectRatio) * width;
+                float right = x + width;
+                if (draw)
+                {
+                    DrawChar(charInfo, x, right, y + height * (charInfo.Height - charInfo.BearingY), y - height * charInfo.BearingY);
+                }
+                x = right + charInfo.Advance * charInfo.AspectRatio + 1;
+            }
+
+            if (draw)
+            {
+                GL.End();
+
+                // Cleanup for caller
+                GL.BindTexture(TextureTarget.Texture2D, 0);
+                GL.Disable(EnableCap.Texture2D);
+                GL.Disable(EnableCap.Blend);
+            }
+
+            // Return width of rendered text
+            return x - originalX;
+        }
+
+        private void DrawChar(CharacterInfo charInfo, float left, float right, float top, float bottom)
+        {
+            GL.TexCoord2(charInfo.Left, charInfo.Bottom);  GL.Vertex2(left, bottom);
+            GL.TexCoord2(charInfo.Left, charInfo.Top);     GL.Vertex2(left, top);
+            GL.TexCoord2(charInfo.Right, charInfo.Top);    GL.Vertex2(right, top);
+
+            GL.TexCoord2(charInfo.Right, charInfo.Top);    GL.Vertex2(right, top);
+            GL.TexCoord2(charInfo.Right, charInfo.Bottom); GL.Vertex2(right, bottom);
+            GL.TexCoord2(charInfo.Left, charInfo.Bottom);  GL.Vertex2(left, bottom);
+        }
+
+        public unsafe Surface RenderSurface(char c, FontFace font, out float xBearing, out float yBearing, out float advance)
+        {
+            var glyph = font.GetGlyph(c, 64);
+            xBearing  = glyph.HorizontalMetrics.Bearing.X;
+            yBearing  = glyph.RenderHeight - glyph.HorizontalMetrics.Bearing.Y;
+            advance   = glyph.HorizontalMetrics.Advance;
+
+            var surface = new Surface
+            {
+                Bits   = Marshal.AllocHGlobal(glyph.RenderWidth * glyph.RenderHeight),
+                Width  = glyph.RenderWidth,
+                Height = glyph.RenderHeight,
+                Pitch  = glyph.RenderWidth
+            };
+
+            var stuff = (byte*)surface.Bits;
+            for (int i = 0; i < surface.Width * surface.Height; i++)
+                *stuff++ = 0;
+
+            glyph.RenderTo(surface);
+
+            return surface;
+        }
+
+        private CharacterInfo UpdateTexture(Surface surface, ref uint[] rawCharMap, ref int posX, ref int posY, ref int lineOffset)
+        {
+            int width   = surface.Width;
+            int height  = surface.Height;
+            int len     = width * height;
+            byte[] data = new byte[len];
+
+            // Get character bitmap
+            Marshal.Copy(surface.Bits, data, 0, len);
+
+            // Find a slot
+            if (posX + width > SheetWidth)
+            {
+                posX       = 0;
+                posY      += lineOffset;
+                lineOffset = 0;
+            }
+
+            // Update lineoffset
+            if (lineOffset < height)
+            {
+                lineOffset = height + 1;
+            }
+
+            // Copy char to sheet
+            for (int y = 0; y < height; y++)
+            {
+                int destOffset   = (y + posY) * SheetWidth + posX;
+                int sourceOffset = y * width;
+
+                for (int x = 0; x < width; x++)
+                {
+                    rawCharMap[destOffset + x] = (uint)((0xFFFFFF << 8) | data[sourceOffset + x]);
+                }
+            }
+
+            // Generate character info
+            CharacterInfo charInfo = new CharacterInfo()
+            {
+                Left   = (float)posX / SheetWidth,
+                Right  = (float)(posX + width) / SheetWidth,
+                Top    = (float)(posY - 1) / SheetHeight,
+                Bottom = (float)(posY + height) / SheetHeight,
+                Width  = width,
+                Height = height,
+            };
+
+            // Update x
+            posX += width + 1;
+
+            // Give the memory back
+            Marshal.FreeHGlobal(surface.Bits);
+            return charInfo;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj b/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj
index 18452f0a..04cab832 100644
--- a/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj
+++ b/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj
@@ -4,6 +4,17 @@
     <TargetFramework>netcoreapp2.1</TargetFramework>
     <RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
     <OutputType>Exe</OutputType>
+    <Configurations>Debug;Release;Profile Debug;Profile Release</Configurations>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>false</Optimize>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj b/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj
index ee7c103d..5a99b39f 100644
--- a/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj
+++ b/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj
@@ -4,12 +4,23 @@
     <TargetFramework>netcoreapp2.1</TargetFramework>
     <RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <Configurations>Debug;Release;Profile Debug;Profile Release</Configurations>
   </PropertyGroup>
 
   <PropertyGroup>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>false</Optimize>
+  </PropertyGroup>
+
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
     <PackageReference Include="System.Runtime.Intrinsics.Experimental" Version="4.5.0-rc1" />
diff --git a/Ryujinx.Tests/Ryujinx.Tests.csproj b/Ryujinx.Tests/Ryujinx.Tests.csproj
index ce94326d..9ddeb314 100644
--- a/Ryujinx.Tests/Ryujinx.Tests.csproj
+++ b/Ryujinx.Tests/Ryujinx.Tests.csproj
@@ -9,12 +9,23 @@
     <TargetOS Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">windows</TargetOS>
     <TargetOS Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">osx</TargetOS>
     <TargetOS Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">linux</TargetOS>
+    <Configurations>Debug;Release;Profile Debug;Profile Release</Configurations>
   </PropertyGroup>
 
   <PropertyGroup>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>false</Optimize>
+  </PropertyGroup>
+
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
     <PackageReference Include="NUnit" Version="3.11.0" />
diff --git a/Ryujinx.sln b/Ryujinx.sln
index 990a89a2..b928a06d 100644
--- a/Ryujinx.sln
+++ b/Ryujinx.sln
@@ -10,6 +10,9 @@ EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests.Unicorn", "Ryujinx.Tests.Unicorn\Ryujinx.Tests.Unicorn.csproj", "{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE", "Ryujinx.HLE\Ryujinx.HLE.csproj", "{CB92CFF9-1D62-4D4F-9E88-8130EF61E351}"
+	ProjectSection(ProjectDependencies) = postProject
+		{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34} = {4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}
+	EndProjectSection
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChocolArm64", "ChocolArm64\ChocolArm64.csproj", "{2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}"
 EndProject
@@ -23,54 +26,106 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Luea", "Ryujinx.LLE\Luea.cs
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Common", "Ryujinx.Common\Ryujinx.Common.csproj", "{5FD4E4F6-8928-4B3C-BE07-28A675C17226}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Profiler", "Ryujinx.Profiler\Ryujinx.Profiler.csproj", "{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}"
+EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{464D8AB7-B056-4A99-B207-B8DCFB47AAA9}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
+		Profile Debug|Any CPU = Profile Debug|Any CPU
+		Profile Release|Any CPU = Profile Release|Any CPU
 		Release|Any CPU = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
 		{074045D4-3ED2-4711-9169-E385F2BFB5A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{074045D4-3ED2-4711-9169-E385F2BFB5A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{074045D4-3ED2-4711-9169-E385F2BFB5A0}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
+		{074045D4-3ED2-4711-9169-E385F2BFB5A0}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
+		{074045D4-3ED2-4711-9169-E385F2BFB5A0}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
+		{074045D4-3ED2-4711-9169-E385F2BFB5A0}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
 		{074045D4-3ED2-4711-9169-E385F2BFB5A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{074045D4-3ED2-4711-9169-E385F2BFB5A0}.Release|Any CPU.Build.0 = Release|Any CPU
 		{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
+		{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
+		{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
+		{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
 		{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}.Release|Any CPU.Build.0 = Release|Any CPU
 		{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
+		{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
+		{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
+		{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
 		{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{D8F72938-78EF-4E8C-BAFE-531C9C3C8F15}.Release|Any CPU.Build.0 = Release|Any CPU
 		{CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
+		{CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
+		{CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
+		{CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
 		{CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{CB92CFF9-1D62-4D4F-9E88-8130EF61E351}.Release|Any CPU.Build.0 = Release|Any CPU
 		{2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
+		{2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
+		{2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
+		{2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
 		{2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{2345A1A7-8DEF-419B-9AFB-4DFD41D20D05}.Release|Any CPU.Build.0 = Release|Any CPU
 		{EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
+		{EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
+		{EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
+		{EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
 		{EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{EAAE36AF-7781-4578-A7E0-F0EFD2025569}.Release|Any CPU.Build.0 = Release|Any CPU
 		{5C1D818E-682A-46A5-9D54-30006E26C270}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{5C1D818E-682A-46A5-9D54-30006E26C270}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{5C1D818E-682A-46A5-9D54-30006E26C270}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
+		{5C1D818E-682A-46A5-9D54-30006E26C270}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
+		{5C1D818E-682A-46A5-9D54-30006E26C270}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
+		{5C1D818E-682A-46A5-9D54-30006E26C270}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
 		{5C1D818E-682A-46A5-9D54-30006E26C270}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{5C1D818E-682A-46A5-9D54-30006E26C270}.Release|Any CPU.Build.0 = Release|Any CPU
 		{3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
+		{3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
+		{3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
+		{3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
 		{3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{3AB294D0-2230-468F-9EB3-BDFCAEAE99A5}.Release|Any CPU.Build.0 = Release|Any CPU
 		{8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
+		{8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
+		{8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
+		{8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
 		{8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{8E7D36DD-9626-47E2-8EF5-8F2F66751C9C}.Release|Any CPU.Build.0 = Release|Any CPU
 		{5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
+		{5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
+		{5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
+		{5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
 		{5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{5FD4E4F6-8928-4B3C-BE07-28A675C17226}.Release|Any CPU.Build.0 = Release|Any CPU
+		{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Profile Debug|Any CPU.ActiveCfg = Profile Debug|Any CPU
+		{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Profile Debug|Any CPU.Build.0 = Profile Debug|Any CPU
+		{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Profile Release|Any CPU.ActiveCfg = Profile Release|Any CPU
+		{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Profile Release|Any CPU.Build.0 = Profile Release|Any CPU
+		{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{4E69B67F-8CA7-42CF-A9E1-CCB0915DFB34}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs
index 42a6a741..a72cd39e 100644
--- a/Ryujinx/Program.cs
+++ b/Ryujinx/Program.cs
@@ -3,6 +3,7 @@ using Ryujinx.Common.Logging;
 using Ryujinx.Graphics.Gal;
 using Ryujinx.Graphics.Gal.OpenGL;
 using Ryujinx.HLE;
+using Ryujinx.Profiler;
 using System;
 using System.IO;
 
@@ -25,6 +26,8 @@ namespace Ryujinx
             Configuration.Load(Path.Combine(ApplicationDirectory, "Config.jsonc"));
             Configuration.Configure(device);
 
+            Profile.Initalize();
+
             AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
             AppDomain.CurrentDomain.ProcessExit        += CurrentDomain_ProcessExit;
 
@@ -89,6 +92,8 @@ namespace Ryujinx
             {
                 screen.MainLoop();
 
+                Profile.FinishProfiling();
+
                 device.Dispose();
             }
 
diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj
index 08725846..ab0ee599 100644
--- a/Ryujinx/Ryujinx.csproj
+++ b/Ryujinx/Ryujinx.csproj
@@ -5,6 +5,17 @@
     <RuntimeIdentifiers>win10-x64;osx-x64;linux-x64</RuntimeIdentifiers>
     <OutputType>Exe</OutputType>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <Configurations>Debug;Release;Profile Debug;Profile Release</Configurations>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Release|AnyCPU'">
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile Debug|AnyCPU'">
+    <DefineConstants>TRACE;USE_PROFILING</DefineConstants>
+    <Optimize>false</Optimize>
   </PropertyGroup>
 
   <ItemGroup>
@@ -17,6 +28,7 @@
     <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
     <ProjectReference Include="..\Ryujinx.Graphics\Ryujinx.Graphics.csproj" />
     <ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
+    <ProjectReference Include="..\Ryujinx.Profiler\Ryujinx.Profiler.csproj" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/Ryujinx/Ui/GLScreen.cs b/Ryujinx/Ui/GLScreen.cs
index d9661237..c4fe65ab 100644
--- a/Ryujinx/Ui/GLScreen.cs
+++ b/Ryujinx/Ui/GLScreen.cs
@@ -4,6 +4,8 @@ using OpenTK.Input;
 using Ryujinx.Graphics.Gal;
 using Ryujinx.HLE;
 using Ryujinx.HLE.Input;
+using Ryujinx.Profiler;
+using Ryujinx.Profiler.UI;
 using System;
 using System.Threading;
 
@@ -36,6 +38,10 @@ namespace Ryujinx
 
         private string _newTitle;
 
+#if USE_PROFILING
+        private ProfileWindowManager _profileWindow;
+#endif
+
         public GlScreen(Switch device, IGalRenderer renderer)
             : base(1280, 720,
             new GraphicsMode(), "Ryujinx", 0,
@@ -48,6 +54,11 @@ namespace Ryujinx
             Location = new Point(
                 (DisplayDevice.Default.Width  / 2) - (Width  / 2),
                 (DisplayDevice.Default.Height / 2) - (Height / 2));
+            
+#if USE_PROFILING
+            // Start profile window, it will handle itself from there
+            _profileWindow = new ProfileWindowManager();
+#endif
         }
 
         private void RenderLoop()
@@ -145,6 +156,12 @@ namespace Ryujinx
             {
                 KeyboardState keyboard = _keyboard.Value;
 
+#if USE_PROFILING
+                // Profiler input, lets the profiler get access to the main windows keyboard state
+                _profileWindow.UpdateKeyInput(keyboard);
+#endif
+
+                // Normal Input
                 currentHotkeyButtons = Configuration.Instance.KeyboardControls.GetHotkeyButtons(keyboard);
                 currentButton        = Configuration.Instance.KeyboardControls.GetButtons(keyboard);
 
@@ -278,6 +295,10 @@ namespace Ryujinx
 
         protected override void OnUnload(EventArgs e)
         {
+#if USE_PROFILING
+            _profileWindow.Close();
+#endif
+
             _renderThread.Join();
 
             base.OnUnload(e);
@@ -336,4 +357,4 @@ namespace Ryujinx
             _mouse = e.Mouse;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/appveyor.yml b/appveyor.yml
index b29a9233..a1201aa6 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -3,26 +3,32 @@ branches:
   only:
   - master
 image: Visual Studio 2017
-configuration: Release
+environment:
+  matrix:
+    - config: Release
+      config_name: '-'
+
+    - config: Profile Release
+      config_name: '-profiled-'
 build_script:
 - ps: >-
     dotnet --version
 
-    dotnet publish -c Release -r win-x64
+    dotnet publish -c $env:config -r win-x64
     
-    dotnet publish -c Release -r linux-x64
+    dotnet publish -c $env:config -r linux-x64
     
-    dotnet publish -c Release -r osx-x64
+    dotnet publish -c $env:config -r osx-x64
 
-    7z a ryujinx-$env:APPVEYOR_BUILD_VERSION-win_x64.zip $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\Release\netcoreapp2.1\win-x64\publish\
+    7z a ryujinx$env:config_name$env:APPVEYOR_BUILD_VERSION-win_x64.zip $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\$env:config\netcoreapp2.1\win-x64\publish\
 
-    7z a ryujinx-$env:APPVEYOR_BUILD_VERSION-linux_x64.tar $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\Release\netcoreapp2.1\linux-x64\publish\
+    7z a ryujinx$env:config_name$env:APPVEYOR_BUILD_VERSION-linux_x64.tar $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\$env:config\netcoreapp2.1\linux-x64\publish\
 
-    7z a ryujinx-$env:APPVEYOR_BUILD_VERSION-linux_x64.tar.gz ryujinx-$env:APPVEYOR_BUILD_VERSION-linux_x64.tar
+    7z a ryujinx$env:config_name$env:APPVEYOR_BUILD_VERSION-linux_x64.tar.gz ryujinx$env:config_name$env:APPVEYOR_BUILD_VERSION-linux_x64.tar
 
-    7z a ryujinx-$env:APPVEYOR_BUILD_VERSION-osx_x64.zip $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\Release\netcoreapp2.1\osx-x64\publish\
+    7z a ryujinx$env:config_name$env:APPVEYOR_BUILD_VERSION-osx_x64.zip $env:APPVEYOR_BUILD_FOLDER\Ryujinx\bin\$env:config\netcoreapp2.1\osx-x64\publish\
 
 artifacts:
-- path: ryujinx-%APPVEYOR_BUILD_VERSION%-win_x64.zip
-- path: ryujinx-%APPVEYOR_BUILD_VERSION%-linux_x64.tar.gz
-- path: ryujinx-%APPVEYOR_BUILD_VERSION%-osx_x64.zip
+- path: ryujinx%config_name%%APPVEYOR_BUILD_VERSION%-win_x64.zip
+- path: ryujinx%config_name%%APPVEYOR_BUILD_VERSION%-linux_x64.tar.gz
+- path: ryujinx%config_name%%APPVEYOR_BUILD_VERSION%-osx_x64.zip