diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt
index d4460bf01..15a6ccf9a 100644
--- a/src/citra_qt/CMakeLists.txt
+++ b/src/citra_qt/CMakeLists.txt
@@ -69,7 +69,6 @@ set(HEADERS
 set(UIS
             debugger/callstack.ui
             debugger/disassembler.ui
-            debugger/profiler.ui
             debugger/registers.ui
             configure.ui
             configure_audio.ui
diff --git a/src/citra_qt/config.cpp b/src/citra_qt/config.cpp
index b65f57fdc..5fe57dfa2 100644
--- a/src/citra_qt/config.cpp
+++ b/src/citra_qt/config.cpp
@@ -146,6 +146,7 @@ void Config::ReadValues() {
 
     UISettings::values.single_window_mode = qt_config->value("singleWindowMode", true).toBool();
     UISettings::values.display_titlebar = qt_config->value("displayTitleBars", true).toBool();
+    UISettings::values.show_status_bar = qt_config->value("showStatusBar", true).toBool();
     UISettings::values.confirm_before_closing = qt_config->value("confirmClose", true).toBool();
     UISettings::values.first_start = qt_config->value("firstStart", true).toBool();
 
@@ -252,6 +253,7 @@ void Config::SaveValues() {
 
     qt_config->setValue("singleWindowMode", UISettings::values.single_window_mode);
     qt_config->setValue("displayTitleBars", UISettings::values.display_titlebar);
+    qt_config->setValue("showStatusBar", UISettings::values.show_status_bar);
     qt_config->setValue("confirmClose", UISettings::values.confirm_before_closing);
     qt_config->setValue("firstStart", UISettings::values.first_start);
 
diff --git a/src/citra_qt/configure_system.cpp b/src/citra_qt/configure_system.cpp
index eb1276ef3..040185e82 100644
--- a/src/citra_qt/configure_system.cpp
+++ b/src/citra_qt/configure_system.cpp
@@ -4,6 +4,7 @@
 
 #include "citra_qt/configure_system.h"
 #include "citra_qt/ui_settings.h"
+#include "core/core.h"
 #include "core/hle/service/cfg/cfg.h"
 #include "core/hle/service/fs/archive.h"
 #include "ui_configure_system.h"
diff --git a/src/citra_qt/debugger/profiler.cpp b/src/citra_qt/debugger/profiler.cpp
index cee10403d..f060bbe08 100644
--- a/src/citra_qt/debugger/profiler.cpp
+++ b/src/citra_qt/debugger/profiler.cpp
@@ -2,6 +2,8 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
+#include <QAction>
+#include <QLayout>
 #include <QMouseEvent>
 #include <QPainter>
 #include <QString>
@@ -9,121 +11,12 @@
 #include "citra_qt/util/util.h"
 #include "common/common_types.h"
 #include "common/microprofile.h"
-#include "common/profiler_reporting.h"
 
 // Include the implementation of the UI in this file. This isn't in microprofile.cpp because the
 // non-Qt frontends don't need it (and don't implement the UI drawing hooks either).
 #if MICROPROFILE_ENABLED
 #define MICROPROFILEUI_IMPL 1
 #include "common/microprofileui.h"
-#endif
-
-using namespace Common::Profiling;
-
-static QVariant GetDataForColumn(int col, const AggregatedDuration& duration) {
-    static auto duration_to_float = [](Duration dur) -> float {
-        using FloatMs = std::chrono::duration<float, std::chrono::milliseconds::period>;
-        return std::chrono::duration_cast<FloatMs>(dur).count();
-    };
-
-    switch (col) {
-    case 1:
-        return duration_to_float(duration.avg);
-    case 2:
-        return duration_to_float(duration.min);
-    case 3:
-        return duration_to_float(duration.max);
-    default:
-        return QVariant();
-    }
-}
-
-ProfilerModel::ProfilerModel(QObject* parent) : QAbstractItemModel(parent) {
-    updateProfilingInfo();
-}
-
-QVariant ProfilerModel::headerData(int section, Qt::Orientation orientation, int role) const {
-    if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
-        switch (section) {
-        case 0:
-            return tr("Category");
-        case 1:
-            return tr("Avg");
-        case 2:
-            return tr("Min");
-        case 3:
-            return tr("Max");
-        }
-    }
-
-    return QVariant();
-}
-
-QModelIndex ProfilerModel::index(int row, int column, const QModelIndex& parent) const {
-    return createIndex(row, column);
-}
-
-QModelIndex ProfilerModel::parent(const QModelIndex& child) const {
-    return QModelIndex();
-}
-
-int ProfilerModel::columnCount(const QModelIndex& parent) const {
-    return 4;
-}
-
-int ProfilerModel::rowCount(const QModelIndex& parent) const {
-    if (parent.isValid()) {
-        return 0;
-    } else {
-        return 2;
-    }
-}
-
-QVariant ProfilerModel::data(const QModelIndex& index, int role) const {
-    if (role == Qt::DisplayRole) {
-        if (index.row() == 0) {
-            if (index.column() == 0) {
-                return tr("Frame");
-            } else {
-                return GetDataForColumn(index.column(), results.frame_time);
-            }
-        } else if (index.row() == 1) {
-            if (index.column() == 0) {
-                return tr("Frame (with swapping)");
-            } else {
-                return GetDataForColumn(index.column(), results.interframe_time);
-            }
-        }
-    }
-
-    return QVariant();
-}
-
-void ProfilerModel::updateProfilingInfo() {
-    results = GetTimingResultsAggregator()->GetAggregatedResults();
-    emit dataChanged(createIndex(0, 1), createIndex(rowCount() - 1, 3));
-}
-
-ProfilerWidget::ProfilerWidget(QWidget* parent) : QDockWidget(parent) {
-    ui.setupUi(this);
-
-    model = new ProfilerModel(this);
-    ui.treeView->setModel(model);
-
-    connect(this, SIGNAL(visibilityChanged(bool)), SLOT(setProfilingInfoUpdateEnabled(bool)));
-    connect(&update_timer, SIGNAL(timeout()), model, SLOT(updateProfilingInfo()));
-}
-
-void ProfilerWidget::setProfilingInfoUpdateEnabled(bool enable) {
-    if (enable) {
-        update_timer.start(100);
-        model->updateProfilingInfo();
-    } else {
-        update_timer.stop();
-    }
-}
-
-#if MICROPROFILE_ENABLED
 
 class MicroProfileWidget : public QWidget {
 public:
diff --git a/src/citra_qt/debugger/profiler.h b/src/citra_qt/debugger/profiler.h
index c8912fd5a..eae1e9e3c 100644
--- a/src/citra_qt/debugger/profiler.h
+++ b/src/citra_qt/debugger/profiler.h
@@ -8,46 +8,6 @@
 #include <QDockWidget>
 #include <QTimer>
 #include "common/microprofile.h"
-#include "common/profiler_reporting.h"
-#include "ui_profiler.h"
-
-class ProfilerModel : public QAbstractItemModel {
-    Q_OBJECT
-
-public:
-    explicit ProfilerModel(QObject* parent);
-
-    QVariant headerData(int section, Qt::Orientation orientation,
-                        int role = Qt::DisplayRole) const override;
-    QModelIndex index(int row, int column,
-                      const QModelIndex& parent = QModelIndex()) const override;
-    QModelIndex parent(const QModelIndex& child) const override;
-    int columnCount(const QModelIndex& parent = QModelIndex()) const override;
-    int rowCount(const QModelIndex& parent = QModelIndex()) const override;
-    QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
-
-public slots:
-    void updateProfilingInfo();
-
-private:
-    Common::Profiling::AggregatedFrameResult results;
-};
-
-class ProfilerWidget : public QDockWidget {
-    Q_OBJECT
-
-public:
-    explicit ProfilerWidget(QWidget* parent = nullptr);
-
-private slots:
-    void setProfilingInfoUpdateEnabled(bool enable);
-
-private:
-    Ui::Profiler ui;
-    ProfilerModel* model;
-
-    QTimer update_timer;
-};
 
 class MicroProfileDialog : public QWidget {
     Q_OBJECT
diff --git a/src/citra_qt/debugger/profiler.ui b/src/citra_qt/debugger/profiler.ui
deleted file mode 100644
index d3c9a9a1f..000000000
--- a/src/citra_qt/debugger/profiler.ui
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>Profiler</class>
- <widget class="QDockWidget" name="Profiler">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>400</width>
-    <height>300</height>
-   </rect>
-  </property>
-  <property name="windowTitle">
-   <string>Profiler</string>
-  </property>
-  <widget class="QWidget" name="dockWidgetContents">
-   <layout class="QVBoxLayout" name="verticalLayout">
-    <item>
-     <widget class="QTreeView" name="treeView">
-      <property name="alternatingRowColors">
-       <bool>true</bool>
-      </property>
-      <property name="uniformRowHeights">
-       <bool>true</bool>
-      </property>
-     </widget>
-    </item>
-   </layout>
-  </widget>
- </widget>
- <resources/>
- <connections/>
-</ui>
diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp
index db6f920ff..a9ec9e830 100644
--- a/src/citra_qt/game_list.cpp
+++ b/src/citra_qt/game_list.cpp
@@ -45,6 +45,7 @@ GameList::GameList(QWidget* parent) : QWidget{parent} {
     // with signals/slots. In this case, QList falls under the umbrells of custom types.
     qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
 
+    layout->setContentsMargins(0, 0, 0, 0);
     layout->addWidget(tree_view);
     setLayout(layout);
 }
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index 7a80af890..fd51659b9 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -95,6 +95,26 @@ void GMainWindow::InitializeWidgets() {
 
     game_list = new GameList();
     ui.horizontalLayout->addWidget(game_list);
+
+    // Create status bar
+    emu_speed_label = new QLabel();
+    emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% "
+                                   "indicate emulation is running faster or slower than a 3DS."));
+    game_fps_label = new QLabel();
+    game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. "
+                                  "This will vary from game to game and scene to scene."));
+    emu_frametime_label = new QLabel();
+    emu_frametime_label->setToolTip(
+        tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For "
+           "full-speed emulation this should be at most 16.67 ms."));
+
+    for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) {
+        label->setVisible(false);
+        label->setFrameStyle(QFrame::NoFrame);
+        label->setContentsMargins(4, 0, 4, 0);
+        statusBar()->addPermanentWidget(label);
+    }
+    statusBar()->setVisible(true);
 }
 
 void GMainWindow::InitializeDebugWidgets() {
@@ -103,11 +123,6 @@ void GMainWindow::InitializeDebugWidgets() {
 
     QMenu* debug_menu = ui.menu_View_Debugging;
 
-    profilerWidget = new ProfilerWidget(this);
-    addDockWidget(Qt::BottomDockWidgetArea, profilerWidget);
-    profilerWidget->hide();
-    debug_menu->addAction(profilerWidget->toggleViewAction());
-
 #if MICROPROFILE_ENABLED
     microProfileDialog = new MicroProfileDialog(this);
     microProfileDialog->hide();
@@ -230,6 +245,9 @@ void GMainWindow::RestoreUIState() {
 
     ui.action_Display_Dock_Widget_Headers->setChecked(UISettings::values.display_titlebar);
     OnDisplayTitleBars(ui.action_Display_Dock_Widget_Headers->isChecked());
+
+    ui.action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar);
+    statusBar()->setVisible(ui.action_Show_Status_Bar->isChecked());
 }
 
 void GMainWindow::ConnectWidgetEvents() {
@@ -240,6 +258,8 @@ void GMainWindow::ConnectWidgetEvents() {
     connect(this, SIGNAL(EmulationStarting(EmuThread*)), render_window,
             SLOT(OnEmulationStarting(EmuThread*)));
     connect(this, SIGNAL(EmulationStopping()), render_window, SLOT(OnEmulationStopping()));
+
+    connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar);
 }
 
 void GMainWindow::ConnectMenuEvents() {
@@ -262,6 +282,7 @@ void GMainWindow::ConnectMenuEvents() {
             &GMainWindow::ToggleWindowMode);
     connect(ui.action_Display_Dock_Widget_Headers, &QAction::triggered, this,
             &GMainWindow::OnDisplayTitleBars);
+    connect(ui.action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible);
 }
 
 void GMainWindow::OnDisplayTitleBars(bool show) {
@@ -387,6 +408,8 @@ void GMainWindow::BootGame(const QString& filename) {
     if (ui.action_Single_Window_Mode->isChecked()) {
         game_list->hide();
     }
+    status_bar_update_timer.start(2000);
+
     render_window->show();
     render_window->setFocus();
 
@@ -421,6 +444,12 @@ void GMainWindow::ShutdownGame() {
     render_window->hide();
     game_list->show();
 
+    // Disable status bar updates
+    status_bar_update_timer.stop();
+    emu_speed_label->setVisible(false);
+    game_fps_label->setVisible(false);
+    emu_frametime_label->setVisible(false);
+
     emulation_running = false;
 }
 
@@ -600,6 +629,23 @@ void GMainWindow::OnCreateGraphicsSurfaceViewer() {
     graphicsSurfaceViewerWidget->show();
 }
 
+void GMainWindow::UpdateStatusBar() {
+    if (emu_thread == nullptr) {
+        status_bar_update_timer.stop();
+        return;
+    }
+
+    auto results = Core::System::GetInstance().GetAndResetPerfStats();
+
+    emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0));
+    game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0));
+    emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2));
+
+    emu_speed_label->setVisible(true);
+    game_fps_label->setVisible(true);
+    emu_frametime_label->setVisible(true);
+}
+
 bool GMainWindow::ConfirmClose() {
     if (emu_thread == nullptr || !UISettings::values.confirm_before_closing)
         return true;
@@ -625,6 +671,7 @@ void GMainWindow::closeEvent(QCloseEvent* event) {
 #endif
     UISettings::values.single_window_mode = ui.action_Single_Window_Mode->isChecked();
     UISettings::values.display_titlebar = ui.action_Display_Dock_Widget_Headers->isChecked();
+    UISettings::values.show_status_bar = ui.action_Show_Status_Bar->isChecked();
     UISettings::values.first_start = false;
 
     game_list->SaveInterfaceLayout();
diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h
index 87637b92b..ec841eaa5 100644
--- a/src/citra_qt/main.h
+++ b/src/citra_qt/main.h
@@ -127,17 +127,26 @@ private slots:
     void OnCreateGraphicsSurfaceViewer();
 
 private:
+    void UpdateStatusBar();
+
     Ui::MainWindow ui;
 
     GRenderWindow* render_window;
     GameList* game_list;
 
+    // Status bar elements
+    QLabel* emu_speed_label = nullptr;
+    QLabel* game_fps_label = nullptr;
+    QLabel* emu_frametime_label = nullptr;
+    QTimer status_bar_update_timer;
+
     std::unique_ptr<Config> config;
 
     // Whether emulation is currently running in Citra.
     bool emulation_running = false;
     std::unique_ptr<EmuThread> emu_thread;
 
+    // Debugger panes
     ProfilerWidget* profilerWidget;
     MicroProfileDialog* microProfileDialog;
     DisassemblerWidget* disasmWidget;
diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui
index 4a95cda9a..47dbb6ef7 100644
--- a/src/citra_qt/main.ui
+++ b/src/citra_qt/main.ui
@@ -88,6 +88,7 @@
     </widget>
     <addaction name="action_Single_Window_Mode"/>
     <addaction name="action_Display_Dock_Widget_Headers"/>
+    <addaction name="action_Show_Status_Bar"/>
     <addaction name="menu_View_Debugging"/>
    </widget>
    <widget class="QMenu" name="menu_Help">
@@ -101,7 +102,6 @@
    <addaction name="menu_View"/>
    <addaction name="menu_Help"/>
   </widget>
-  <widget class="QStatusBar" name="statusbar"/>
   <action name="action_Load_File">
    <property name="text">
     <string>Load File...</string>
@@ -167,6 +167,14 @@
     <string>Display Dock Widget Headers</string>
    </property>
   </action>
+  <action name="action_Show_Status_Bar">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Show Status Bar</string>
+   </property>
+  </action>
   <action name="action_Select_Game_List_Root">
    <property name="text">
     <string>Select Game Directory...</string>
diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h
index ed7fdff7e..6408ece2b 100644
--- a/src/citra_qt/ui_settings.h
+++ b/src/citra_qt/ui_settings.h
@@ -27,6 +27,7 @@ struct Values {
 
     bool single_window_mode;
     bool display_titlebar;
+    bool show_status_bar;
 
     bool confirm_before_closing;
     bool first_start;
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 26c83efda..8a6170257 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -35,7 +35,6 @@ set(SRCS
             memory_util.cpp
             microprofile.cpp
             misc.cpp
-            profiler.cpp
             scm_rev.cpp
             string_util.cpp
             symbols.cpp
@@ -68,7 +67,6 @@ set(HEADERS
             microprofile.h
             microprofileui.h
             platform.h
-            profiler_reporting.h
             quaternion.h
             scm_rev.h
             scope_exit.h
diff --git a/src/common/profiler.cpp b/src/common/profiler.cpp
deleted file mode 100644
index b40e7205d..000000000
--- a/src/common/profiler.cpp
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright 2015 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-#include <algorithm>
-#include <cstddef>
-#include <vector>
-#include "common/assert.h"
-#include "common/profiler_reporting.h"
-#include "common/synchronized_wrapper.h"
-
-namespace Common {
-namespace Profiling {
-
-ProfilingManager::ProfilingManager()
-    : last_frame_end(Clock::now()), this_frame_start(Clock::now()) {}
-
-void ProfilingManager::BeginFrame() {
-    this_frame_start = Clock::now();
-}
-
-void ProfilingManager::FinishFrame() {
-    Clock::time_point now = Clock::now();
-
-    results.interframe_time = now - last_frame_end;
-    results.frame_time = now - this_frame_start;
-
-    last_frame_end = now;
-}
-
-TimingResultsAggregator::TimingResultsAggregator(size_t window_size)
-    : max_window_size(window_size), window_size(0) {
-    interframe_times.resize(window_size, Duration::zero());
-    frame_times.resize(window_size, Duration::zero());
-}
-
-void TimingResultsAggregator::Clear() {
-    window_size = cursor = 0;
-}
-
-void TimingResultsAggregator::AddFrame(const ProfilingFrameResult& frame_result) {
-    interframe_times[cursor] = frame_result.interframe_time;
-    frame_times[cursor] = frame_result.frame_time;
-
-    ++cursor;
-    if (cursor == max_window_size)
-        cursor = 0;
-    if (window_size < max_window_size)
-        ++window_size;
-}
-
-static AggregatedDuration AggregateField(const std::vector<Duration>& v, size_t len) {
-    AggregatedDuration result;
-    result.avg = Duration::zero();
-    result.min = result.max = (len == 0 ? Duration::zero() : v[0]);
-
-    for (size_t i = 0; i < len; ++i) {
-        Duration value = v[i];
-        result.avg += value;
-        result.min = std::min(result.min, value);
-        result.max = std::max(result.max, value);
-    }
-    if (len != 0)
-        result.avg /= len;
-
-    return result;
-}
-
-static float tof(Common::Profiling::Duration dur) {
-    using FloatMs = std::chrono::duration<float, std::chrono::milliseconds::period>;
-    return std::chrono::duration_cast<FloatMs>(dur).count();
-}
-
-AggregatedFrameResult TimingResultsAggregator::GetAggregatedResults() const {
-    AggregatedFrameResult result;
-
-    result.interframe_time = AggregateField(interframe_times, window_size);
-    result.frame_time = AggregateField(frame_times, window_size);
-
-    if (result.interframe_time.avg != Duration::zero()) {
-        result.fps = 1000.0f / tof(result.interframe_time.avg);
-    } else {
-        result.fps = 0.0f;
-    }
-
-    return result;
-}
-
-ProfilingManager& GetProfilingManager() {
-    // Takes advantage of "magic" static initialization for race-free initialization.
-    static ProfilingManager manager;
-    return manager;
-}
-
-SynchronizedRef<TimingResultsAggregator> GetTimingResultsAggregator() {
-    static SynchronizedWrapper<TimingResultsAggregator> aggregator(30);
-    return SynchronizedRef<TimingResultsAggregator>(aggregator);
-}
-
-} // namespace Profiling
-} // namespace Common
diff --git a/src/common/profiler_reporting.h b/src/common/profiler_reporting.h
deleted file mode 100644
index e9ce6d41c..000000000
--- a/src/common/profiler_reporting.h
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright 2015 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-#pragma once
-
-#include <chrono>
-#include <cstddef>
-#include <vector>
-#include "common/synchronized_wrapper.h"
-
-namespace Common {
-namespace Profiling {
-
-using Clock = std::chrono::high_resolution_clock;
-using Duration = Clock::duration;
-
-struct ProfilingFrameResult {
-    /// Time since the last delivered frame
-    Duration interframe_time;
-
-    /// Time spent processing a frame, excluding VSync
-    Duration frame_time;
-};
-
-class ProfilingManager final {
-public:
-    ProfilingManager();
-
-    /// This should be called after swapping screen buffers.
-    void BeginFrame();
-    /// This should be called before swapping screen buffers.
-    void FinishFrame();
-
-    /// Get the timing results from the previous frame. This is updated when you call FinishFrame().
-    const ProfilingFrameResult& GetPreviousFrameResults() const {
-        return results;
-    }
-
-private:
-    Clock::time_point last_frame_end;
-    Clock::time_point this_frame_start;
-
-    ProfilingFrameResult results;
-};
-
-struct AggregatedDuration {
-    Duration avg, min, max;
-};
-
-struct AggregatedFrameResult {
-    /// Time since the last delivered frame
-    AggregatedDuration interframe_time;
-
-    /// Time spent processing a frame, excluding VSync
-    AggregatedDuration frame_time;
-
-    float fps;
-};
-
-class TimingResultsAggregator final {
-public:
-    TimingResultsAggregator(size_t window_size);
-
-    void Clear();
-
-    void AddFrame(const ProfilingFrameResult& frame_result);
-
-    AggregatedFrameResult GetAggregatedResults() const;
-
-    size_t max_window_size;
-    size_t window_size;
-    size_t cursor;
-
-    std::vector<Duration> interframe_times;
-    std::vector<Duration> frame_times;
-};
-
-ProfilingManager& GetProfilingManager();
-SynchronizedRef<TimingResultsAggregator> GetTimingResultsAggregator();
-
-} // namespace Profiling
-} // namespace Common
diff --git a/src/common/synchronized_wrapper.h b/src/common/synchronized_wrapper.h
index 04b4f2e51..4a1984c46 100644
--- a/src/common/synchronized_wrapper.h
+++ b/src/common/synchronized_wrapper.h
@@ -9,25 +9,8 @@
 
 namespace Common {
 
-/**
- * Wraps an object, only allowing access to it via a locking reference wrapper. Good to ensure no
- * one forgets to lock a mutex before acessing an object. To access the wrapped object construct a
- * SyncronizedRef on this wrapper. Inspired by Rust's Mutex type
- * (http://doc.rust-lang.org/std/sync/struct.Mutex.html).
- */
 template <typename T>
-class SynchronizedWrapper {
-public:
-    template <typename... Args>
-    SynchronizedWrapper(Args&&... args) : data(std::forward<Args>(args)...) {}
-
-private:
-    template <typename U>
-    friend class SynchronizedRef;
-
-    std::mutex mutex;
-    T data;
-};
+class SynchronizedWrapper;
 
 /**
  * Synchronized reference, that keeps a SynchronizedWrapper's mutex locked during its lifetime. This
@@ -75,4 +58,28 @@ private:
     SynchronizedWrapper<T>* wrapper;
 };
 
+/**
+ * Wraps an object, only allowing access to it via a locking reference wrapper. Good to ensure no
+ * one forgets to lock a mutex before acessing an object. To access the wrapped object construct a
+ * SyncronizedRef on this wrapper. Inspired by Rust's Mutex type
+ * (http://doc.rust-lang.org/std/sync/struct.Mutex.html).
+ */
+template <typename T>
+class SynchronizedWrapper {
+public:
+    template <typename... Args>
+    SynchronizedWrapper(Args&&... args) : data(std::forward<Args>(args)...) {}
+
+    SynchronizedRef<T> Lock() {
+        return {*this};
+    }
+
+private:
+    template <typename U>
+    friend class SynchronizedRef;
+
+    std::mutex mutex;
+    T data;
+};
+
 } // namespace Common
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 8334fece9..ffd67f074 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -173,6 +173,7 @@ set(SRCS
             loader/smdh.cpp
             tracer/recorder.cpp
             memory.cpp
+            perf_stats.cpp
             settings.cpp
             )
 
@@ -363,6 +364,7 @@ set(HEADERS
             memory.h
             memory_setup.h
             mmio.h
+            perf_stats.h
             settings.h
             )
 
diff --git a/src/core/core.cpp b/src/core/core.cpp
index c9c9b7615..140ff6451 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -109,6 +109,10 @@ void System::PrepareReschedule() {
     reschedule_pending = true;
 }
 
+PerfStats::Results System::GetAndResetPerfStats() {
+    return perf_stats.GetAndResetStats(CoreTiming::GetGlobalTimeUs());
+}
+
 void System::Reschedule() {
     if (!reschedule_pending) {
         return;
@@ -140,6 +144,10 @@ System::ResultStatus System::Init(EmuWindow* emu_window, u32 system_mode) {
 
     LOG_DEBUG(Core, "Initialized OK");
 
+    // Reset counters and set time origin to current frame
+    GetAndResetPerfStats();
+    perf_stats.BeginSystemFrame();
+
     return ResultStatus::Success;
 }
 
diff --git a/src/core/core.h b/src/core/core.h
index 17572a74f..6c9c936b5 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -6,9 +6,9 @@
 
 #include <memory>
 #include <string>
-
 #include "common/common_types.h"
 #include "core/memory.h"
+#include "core/perf_stats.h"
 
 class EmuWindow;
 class ARM_Interface;
@@ -83,6 +83,8 @@ public:
     /// Prepare the core emulation for a reschedule
     void PrepareReschedule();
 
+    PerfStats::Results GetAndResetPerfStats();
+
     /**
      * Gets a reference to the emulated CPU.
      * @returns A reference to the emulated CPU.
@@ -91,6 +93,9 @@ public:
         return *cpu_core;
     }
 
+    PerfStats perf_stats;
+    FrameLimiter frame_limiter;
+
 private:
     /**
      * Initialize the emulated system.
diff --git a/src/core/frontend/emu_window.cpp b/src/core/frontend/emu_window.cpp
index 6b4637741..a155b657d 100644
--- a/src/core/frontend/emu_window.cpp
+++ b/src/core/frontend/emu_window.cpp
@@ -5,7 +5,7 @@
 #include <algorithm>
 #include <cmath>
 #include "common/assert.h"
-#include "common/profiler_reporting.h"
+#include "core/core.h"
 #include "core/frontend/emu_window.h"
 #include "core/frontend/key_map.h"
 #include "video_core/video_core.h"
@@ -104,8 +104,7 @@ void EmuWindow::AccelerometerChanged(float x, float y, float z) {
 void EmuWindow::GyroscopeChanged(float x, float y, float z) {
     constexpr float FULL_FPS = 60;
     float coef = GetGyroscopeRawToDpsCoefficient();
-    float stretch =
-        FULL_FPS / Common::Profiling::GetTimingResultsAggregator()->GetAggregatedResults().fps;
+    float stretch = Core::System::GetInstance().perf_stats.GetLastFrameTimeScale();
     std::lock_guard<std::mutex> lock(gyro_mutex);
     gyro_x = static_cast<s16>(x * coef * stretch);
     gyro_y = static_cast<s16>(y * coef * stretch);
diff --git a/src/core/hle/kernel/server_session.h b/src/core/hle/kernel/server_session.h
index c088b9a19..4ffe97b78 100644
--- a/src/core/hle/kernel/server_session.h
+++ b/src/core/hle/kernel/server_session.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#include <memory>
 #include <string>
 #include "common/assert.h"
 #include "common/common_types.h"
diff --git a/src/core/hle/kernel/thread.h b/src/core/hle/kernel/thread.h
index c557a2279..6ab31c70b 100644
--- a/src/core/hle/kernel/thread.h
+++ b/src/core/hle/kernel/thread.h
@@ -11,7 +11,6 @@
 #include <boost/container/flat_set.hpp>
 #include "common/common_types.h"
 #include "core/arm/arm_interface.h"
-#include "core/core.h"
 #include "core/hle/kernel/kernel.h"
 #include "core/hle/result.h"
 
diff --git a/src/core/hle/service/gsp_gpu.cpp b/src/core/hle/service/gsp_gpu.cpp
index 1457518d4..097ed87e4 100644
--- a/src/core/hle/service/gsp_gpu.cpp
+++ b/src/core/hle/service/gsp_gpu.cpp
@@ -4,6 +4,7 @@
 
 #include "common/bit_field.h"
 #include "common/microprofile.h"
+#include "core/core.h"
 #include "core/hle/kernel/event.h"
 #include "core/hle/kernel/shared_memory.h"
 #include "core/hle/result.h"
@@ -280,6 +281,7 @@ ResultCode SetBufferSwap(u32 screen_id, const FrameBufferInfo& info) {
 
     if (screen_id == 0) {
         MicroProfileFlip();
+        Core::System::GetInstance().perf_stats.EndGameFrame();
     }
 
     return RESULT_SUCCESS;
diff --git a/src/core/hle/service/ldr_ro/ldr_ro.cpp b/src/core/hle/service/ldr_ro/ldr_ro.cpp
index 8d00a7577..7af76676b 100644
--- a/src/core/hle/service/ldr_ro/ldr_ro.cpp
+++ b/src/core/hle/service/ldr_ro/ldr_ro.cpp
@@ -6,6 +6,7 @@
 #include "common/common_types.h"
 #include "common/logging/log.h"
 #include "core/arm/arm_interface.h"
+#include "core/core.h"
 #include "core/hle/kernel/process.h"
 #include "core/hle/kernel/vm_manager.h"
 #include "core/hle/service/ldr_ro/cro_helper.h"
diff --git a/src/core/hw/gpu.cpp b/src/core/hw/gpu.cpp
index fa8c13d36..42809c731 100644
--- a/src/core/hw/gpu.cpp
+++ b/src/core/hw/gpu.cpp
@@ -8,17 +8,13 @@
 #include "common/color.h"
 #include "common/common_types.h"
 #include "common/logging/log.h"
-#include "common/math_util.h"
 #include "common/microprofile.h"
-#include "common/thread.h"
-#include "common/timer.h"
 #include "common/vector_math.h"
 #include "core/core_timing.h"
 #include "core/hle/service/gsp_gpu.h"
 #include "core/hw/gpu.h"
 #include "core/hw/hw.h"
 #include "core/memory.h"
-#include "core/settings.h"
 #include "core/tracer/recorder.h"
 #include "video_core/command_processor.h"
 #include "video_core/debug_utils/debug_utils.h"
@@ -32,19 +28,9 @@ namespace GPU {
 Regs g_regs;
 
 /// 268MHz CPU clocks / 60Hz frames per second
-const u64 frame_ticks = BASE_CLOCK_RATE_ARM11 / 60;
+const u64 frame_ticks = BASE_CLOCK_RATE_ARM11 / SCREEN_REFRESH_RATE;
 /// Event id for CoreTiming
 static int vblank_event;
-/// Total number of frames drawn
-static u64 frame_count;
-/// Start clock for frame limiter
-static u32 time_point;
-/// Total delay caused by slow frames
-static float time_delay;
-constexpr float FIXED_FRAME_TIME = 1000.0f / 60;
-// Max lag caused by slow frames. Can be adjusted to compensate for too many slow frames. Higher
-// values increases time needed to limit frame rate after spikes
-constexpr float MAX_LAG_TIME = 18;
 
 template <typename T>
 inline void Read(T& var, const u32 raw_addr) {
@@ -522,24 +508,8 @@ template void Write<u32>(u32 addr, const u32 data);
 template void Write<u16>(u32 addr, const u16 data);
 template void Write<u8>(u32 addr, const u8 data);
 
-static void FrameLimiter() {
-    time_delay += FIXED_FRAME_TIME;
-    time_delay = MathUtil::Clamp(time_delay, -MAX_LAG_TIME, MAX_LAG_TIME);
-    s32 desired_time = static_cast<s32>(time_delay);
-    s32 elapsed_time = static_cast<s32>(Common::Timer::GetTimeMs() - time_point);
-
-    if (elapsed_time < desired_time) {
-        Common::SleepCurrentThread(desired_time - elapsed_time);
-    }
-
-    u32 frame_time = Common::Timer::GetTimeMs() - time_point;
-
-    time_delay -= frame_time;
-}
-
 /// Update hardware
 static void VBlankCallback(u64 userdata, int cycles_late) {
-    frame_count++;
     VideoCore::g_renderer->SwapBuffers();
 
     // Signal to GSP that GPU interrupt has occurred
@@ -550,12 +520,6 @@ static void VBlankCallback(u64 userdata, int cycles_late) {
     Service::GSP::SignalInterrupt(Service::GSP::InterruptId::PDC0);
     Service::GSP::SignalInterrupt(Service::GSP::InterruptId::PDC1);
 
-    if (!Settings::values.use_vsync && Settings::values.toggle_framelimit) {
-        FrameLimiter();
-    }
-
-    time_point = Common::Timer::GetTimeMs();
-
     // Reschedule recurrent event
     CoreTiming::ScheduleEvent(frame_ticks - cycles_late, vblank_event);
 }
@@ -590,9 +554,6 @@ void Init() {
     framebuffer_sub.color_format.Assign(Regs::PixelFormat::RGB8);
     framebuffer_sub.active_fb = 0;
 
-    frame_count = 0;
-    time_point = Common::Timer::GetTimeMs();
-
     vblank_event = CoreTiming::RegisterEvent("GPU::VBlankCallback", VBlankCallback);
     CoreTiming::ScheduleEvent(frame_ticks, vblank_event);
 
diff --git a/src/core/hw/gpu.h b/src/core/hw/gpu.h
index d53381216..bdd997b2a 100644
--- a/src/core/hw/gpu.h
+++ b/src/core/hw/gpu.h
@@ -13,6 +13,8 @@
 
 namespace GPU {
 
+constexpr float SCREEN_REFRESH_RATE = 60;
+
 // Returns index corresponding to the Regs member labeled by field_name
 // TODO: Due to Visual studio bug 209229, offsetof does not return constant expressions
 //       when used with array elements (e.g. GPU_REG_INDEX(memory_fill_config[0])).
diff --git a/src/core/perf_stats.cpp b/src/core/perf_stats.cpp
new file mode 100644
index 000000000..2cdfb9ded
--- /dev/null
+++ b/src/core/perf_stats.cpp
@@ -0,0 +1,105 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <chrono>
+#include <mutex>
+#include <thread>
+#include "common/math_util.h"
+#include "core/hw/gpu.h"
+#include "core/perf_stats.h"
+#include "core/settings.h"
+
+using namespace std::chrono_literals;
+using DoubleSecs = std::chrono::duration<double, std::chrono::seconds::period>;
+using std::chrono::duration_cast;
+using std::chrono::microseconds;
+
+namespace Core {
+
+void PerfStats::BeginSystemFrame() {
+    std::lock_guard<std::mutex> lock(object_mutex);
+
+    frame_begin = Clock::now();
+}
+
+void PerfStats::EndSystemFrame() {
+    std::lock_guard<std::mutex> lock(object_mutex);
+
+    auto frame_end = Clock::now();
+    accumulated_frametime += frame_end - frame_begin;
+    system_frames += 1;
+
+    previous_frame_length = frame_end - previous_frame_end;
+    previous_frame_end = frame_end;
+}
+
+void PerfStats::EndGameFrame() {
+    std::lock_guard<std::mutex> lock(object_mutex);
+
+    game_frames += 1;
+}
+
+PerfStats::Results PerfStats::GetAndResetStats(u64 current_system_time_us) {
+    std::lock_guard<std::mutex> lock(object_mutex);
+
+    auto now = Clock::now();
+    // Walltime elapsed since stats were reset
+    auto interval = duration_cast<DoubleSecs>(now - reset_point).count();
+
+    auto system_us_per_second =
+        static_cast<double>(current_system_time_us - reset_point_system_us) / interval;
+
+    Results results{};
+    results.system_fps = static_cast<double>(system_frames) / interval;
+    results.game_fps = static_cast<double>(game_frames) / interval;
+    results.frametime = duration_cast<DoubleSecs>(accumulated_frametime).count() /
+                        static_cast<double>(system_frames);
+    results.emulation_speed = system_us_per_second / 1'000'000.0;
+
+    // Reset counters
+    reset_point = now;
+    reset_point_system_us = current_system_time_us;
+    accumulated_frametime = Clock::duration::zero();
+    system_frames = 0;
+    game_frames = 0;
+
+    return results;
+}
+
+double PerfStats::GetLastFrameTimeScale() {
+    std::lock_guard<std::mutex> lock(object_mutex);
+
+    constexpr double FRAME_LENGTH = 1.0 / GPU::SCREEN_REFRESH_RATE;
+    return duration_cast<DoubleSecs>(previous_frame_length).count() / FRAME_LENGTH;
+}
+
+void FrameLimiter::DoFrameLimiting(u64 current_system_time_us) {
+    // Max lag caused by slow frames. Can be adjusted to compensate for too many slow frames. Higher
+    // values increase the time needed to recover and limit framerate again after spikes.
+    constexpr microseconds MAX_LAG_TIME_US = 25ms;
+
+    if (!Settings::values.toggle_framelimit) {
+        return;
+    }
+
+    auto now = Clock::now();
+
+    frame_limiting_delta_err += microseconds(current_system_time_us - previous_system_time_us);
+    frame_limiting_delta_err -= duration_cast<microseconds>(now - previous_walltime);
+    frame_limiting_delta_err =
+        MathUtil::Clamp(frame_limiting_delta_err, -MAX_LAG_TIME_US, MAX_LAG_TIME_US);
+
+    if (frame_limiting_delta_err > microseconds::zero()) {
+        std::this_thread::sleep_for(frame_limiting_delta_err);
+
+        auto now_after_sleep = Clock::now();
+        frame_limiting_delta_err -= duration_cast<microseconds>(now_after_sleep - now);
+        now = now_after_sleep;
+    }
+
+    previous_system_time_us = current_system_time_us;
+    previous_walltime = now;
+}
+
+} // namespace Core
diff --git a/src/core/perf_stats.h b/src/core/perf_stats.h
new file mode 100644
index 000000000..362b205c8
--- /dev/null
+++ b/src/core/perf_stats.h
@@ -0,0 +1,83 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <chrono>
+#include <mutex>
+#include "common/common_types.h"
+
+namespace Core {
+
+/**
+ * Class to manage and query performance/timing statistics. All public functions of this class are
+ * thread-safe unless stated otherwise.
+ */
+class PerfStats {
+public:
+    using Clock = std::chrono::high_resolution_clock;
+
+    struct Results {
+        /// System FPS (LCD VBlanks) in Hz
+        double system_fps;
+        /// Game FPS (GSP frame submissions) in Hz
+        double game_fps;
+        /// Walltime per system frame, in seconds, excluding any waits
+        double frametime;
+        /// Ratio of walltime / emulated time elapsed
+        double emulation_speed;
+    };
+
+    void BeginSystemFrame();
+    void EndSystemFrame();
+    void EndGameFrame();
+
+    Results GetAndResetStats(u64 current_system_time_us);
+
+    /**
+     * Gets the ratio between walltime and the emulated time of the previous system frame. This is
+     * useful for scaling inputs or outputs moving between the two time domains.
+     */
+    double GetLastFrameTimeScale();
+
+private:
+    std::mutex object_mutex;
+
+    /// Point when the cumulative counters were reset
+    Clock::time_point reset_point = Clock::now();
+    /// System time when the cumulative counters were reset
+    u64 reset_point_system_us = 0;
+
+    /// Cumulative duration (excluding v-sync/frame-limiting) of frames since last reset
+    Clock::duration accumulated_frametime = Clock::duration::zero();
+    /// Cumulative number of system frames (LCD VBlanks) presented since last reset
+    u32 system_frames = 0;
+    /// Cumulative number of game frames (GSP frame submissions) since last reset
+    u32 game_frames = 0;
+
+    /// Point when the previous system frame ended
+    Clock::time_point previous_frame_end = reset_point;
+    /// Point when the current system frame began
+    Clock::time_point frame_begin = reset_point;
+    /// Total visible duration (including frame-limiting, etc.) of the previous system frame
+    Clock::duration previous_frame_length = Clock::duration::zero();
+};
+
+class FrameLimiter {
+public:
+    using Clock = std::chrono::high_resolution_clock;
+
+    void DoFrameLimiting(u64 current_system_time_us);
+
+private:
+    /// Emulated system time (in microseconds) at the last limiter invocation
+    u64 previous_system_time_us = 0;
+    /// Walltime at the last limiter invocation
+    Clock::time_point previous_walltime = Clock::now();
+
+    /// Accumulated difference between walltime and emulated time
+    std::chrono::microseconds frame_limiting_delta_err{0};
+};
+
+} // namespace Core
diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp
index 2aa90e5c1..e19375466 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.cpp
+++ b/src/video_core/renderer_opengl/renderer_opengl.cpp
@@ -10,8 +10,8 @@
 #include "common/assert.h"
 #include "common/bit_field.h"
 #include "common/logging/log.h"
-#include "common/profiler_reporting.h"
-#include "common/synchronized_wrapper.h"
+#include "core/core.h"
+#include "core/core_timing.h"
 #include "core/frontend/emu_window.h"
 #include "core/hw/gpu.h"
 #include "core/hw/hw.h"
@@ -145,21 +145,16 @@ void RendererOpenGL::SwapBuffers() {
 
     DrawScreens();
 
-    auto& profiler = Common::Profiling::GetProfilingManager();
-    profiler.FinishFrame();
-    {
-        auto aggregator = Common::Profiling::GetTimingResultsAggregator();
-        aggregator->AddFrame(profiler.GetPreviousFrameResults());
-    }
+    Core::System::GetInstance().perf_stats.EndSystemFrame();
 
     // Swap buffers
     render_window->PollEvents();
     render_window->SwapBuffers();
 
+    Core::System::GetInstance().frame_limiter.DoFrameLimiting(CoreTiming::GetGlobalTimeUs());
+    Core::System::GetInstance().perf_stats.BeginSystemFrame();
+
     prev_state.Apply();
-
-    profiler.BeginFrame();
-
     RefreshRasterizerSetting();
 
     if (Pica::g_debug_context && Pica::g_debug_context->recorder) {