[sysprof] libsysprof-ui: land new UI design



commit e8528609ece584e5d3ce3274119044a5110939ee
Author: Christian Hergert <chergert redhat com>
Date:   Mon Jun 24 20:28:18 2019 -0700

    libsysprof-ui: land new UI design
    
    This comprises a massive rewrite of the UI for browsing captures. We use
    the SysprofAid class to scan capture files for content and then auto-
    matically add visualizers and details pages.
    
    To avoid breaking things incrementally, we just land this as a very large
    commit. Not necessarily ideal, but given the amount of stuff that could
    break, this is easier.
    
    As part of this process, we're removing a lot of the surface API so that
    we can limit how much we need to maintain in terms of ABI.

 src/libsysprof-ui/css/SysprofDisplay-shared.css    |   89 ++
 .../css/SysprofVisualizerView-Adwaita-dark.css     |    6 -
 .../css/SysprofVisualizerView-Adwaita.css          |    7 -
 .../css/SysprofVisualizerView-shared.css           |   13 -
 src/libsysprof-ui/libsysprof-ui.gresource.xml      |   16 +-
 src/libsysprof-ui/meson.build                      |   85 +-
 src/libsysprof-ui/rectangles.c                     |   22 +-
 src/libsysprof-ui/sysprof-aid.c                    |   63 +-
 src/libsysprof-ui/sysprof-aid.h                    |   62 +-
 src/libsysprof-ui/sysprof-battery-aid.c            |  239 ++++
 ...epth-visualizer-row.h => sysprof-battery-aid.h} |   10 +-
 src/libsysprof-ui/sysprof-callgraph-aid.c          |  177 +++
 src/libsysprof-ui/sysprof-callgraph-aid.h          |    2 -
 src/libsysprof-ui/sysprof-callgraph-page.c         | 1293 ++++++++++++++++++++
 src/libsysprof-ui/sysprof-callgraph-page.h         |   51 +
 src/libsysprof-ui/sysprof-callgraph-page.ui        |  235 ++++
 src/libsysprof-ui/sysprof-callgraph-view.h         |   12 -
 src/libsysprof-ui/sysprof-capture-view.c           | 1092 -----------------
 src/libsysprof-ui/sysprof-capture-view.h           |   75 --
 src/libsysprof-ui/sysprof-capture-view.ui          |  187 ---
 src/libsysprof-ui/sysprof-cell-renderer-duration.c |    2 +-
 src/libsysprof-ui/sysprof-cell-renderer-percent.c  |    2 +
 src/libsysprof-ui/sysprof-cell-renderer-percent.h  |   13 +-
 src/libsysprof-ui/sysprof-color-cycle.c            |   65 +-
 src/libsysprof-ui/sysprof-color-cycle.h            |   14 +-
 src/libsysprof-ui/sysprof-counters-aid.c           |  248 ++++
 src/libsysprof-ui/sysprof-counters-aid.h           |   33 +
 src/libsysprof-ui/sysprof-cpu-aid.c                |  249 ++++
 src/libsysprof-ui/sysprof-cpu-aid.h                |    2 -
 src/libsysprof-ui/sysprof-cpu-visualizer-row.c     |  239 ----
 src/libsysprof-ui/sysprof-depth-visualizer-row.c   |  357 ------
 src/libsysprof-ui/sysprof-depth-visualizer.c       |  430 +++++++
 src/libsysprof-ui/sysprof-depth-visualizer.h       |   40 +
 src/libsysprof-ui/sysprof-details-page.c           |  324 +++++
 src/libsysprof-ui/sysprof-details-page.h           |   60 +
 src/libsysprof-ui/sysprof-details-page.ui          |  372 ++++++
 src/libsysprof-ui/sysprof-display.c                |  942 ++++++++++----
 src/libsysprof-ui/sysprof-display.h                |   55 +-
 src/libsysprof-ui/sysprof-display.ui               |   40 +-
 src/libsysprof-ui/sysprof-empty-state-view.h       |    6 -
 src/libsysprof-ui/sysprof-failed-state-view.h      |    7 -
 src/libsysprof-ui/sysprof-line-visualizer-row.h    |   70 --
 ...-visualizer-row.c => sysprof-line-visualizer.c} |  313 +++--
 src/libsysprof-ui/sysprof-line-visualizer.h        |   57 +
 src/libsysprof-ui/sysprof-logs-aid.c               |  237 ++++
 src/libsysprof-ui/sysprof-logs-aid.h               |   33 +
 src/libsysprof-ui/sysprof-logs-page.c              |  116 ++
 ...isualizer-row-private.h => sysprof-logs-page.h} |   10 +-
 src/libsysprof-ui/sysprof-logs-page.ui             |   75 ++
 src/libsysprof-ui/sysprof-mark-visualizer-row.c    |  492 --------
 src/libsysprof-ui/sysprof-mark-visualizer-row.h    |   53 -
 src/libsysprof-ui/sysprof-mark-visualizer.c        |  287 +++++
 src/libsysprof-ui/sysprof-mark-visualizer.h        |   47 +
 src/libsysprof-ui/sysprof-marks-aid.c              |  319 +++++
 src/libsysprof-ui/sysprof-marks-aid.h              |   33 +
 .../{sysprof-marks-view.c => sysprof-marks-page.c} |  343 ++++--
 ...f-cpu-visualizer-row.h => sysprof-marks-page.h} |   29 +-
 src/libsysprof-ui/sysprof-marks-page.ui            |  231 ++++
 src/libsysprof-ui/sysprof-marks-view.h             |   55 -
 src/libsysprof-ui/sysprof-marks-view.ui            |   57 -
 src/libsysprof-ui/sysprof-memory-aid.h             |    2 -
 src/libsysprof-ui/sysprof-model-filter.h           |    2 +-
 src/libsysprof-ui/sysprof-notebook.h               |    4 +
 src/libsysprof-ui/sysprof-page.c                   |  235 ++++
 src/libsysprof-ui/sysprof-page.h                   |   86 ++
 src/libsysprof-ui/sysprof-profiler-assistant.c     |   11 +-
 src/libsysprof-ui/sysprof-profiler-assistant.h     |    2 -
 src/libsysprof-ui/sysprof-profiler-assistant.ui    |    8 +-
 src/libsysprof-ui/sysprof-proxy-aid.h              |    3 -
 src/libsysprof-ui/sysprof-recording-state-view.h   |    7 -
 src/libsysprof-ui/sysprof-scrollmap.c              |  306 +++++
 src/libsysprof-ui/sysprof-scrollmap.h              |   37 +
 src/libsysprof-ui/sysprof-theme-manager.c          |    6 +-
 src/libsysprof-ui/sysprof-time-visualizer.c        |  543 ++++++++
 src/libsysprof-ui/sysprof-time-visualizer.h        |   54 +
 src/libsysprof-ui/sysprof-ui-private.h             |   38 +-
 src/libsysprof-ui/sysprof-ui.h                     |   23 +-
 .../sysprof-visualizer-group-header.c              |  159 +++
 .../sysprof-visualizer-group-header.h              |   31 +
 .../sysprof-visualizer-group-private.h             |   48 +
 src/libsysprof-ui/sysprof-visualizer-group.c       |  511 ++++++++
 src/libsysprof-ui/sysprof-visualizer-group.h       |   76 ++
 src/libsysprof-ui/sysprof-visualizer-list.c        |  582 ---------
 src/libsysprof-ui/sysprof-visualizer-list.h        |   60 -
 src/libsysprof-ui/sysprof-visualizer-row.c         |  300 -----
 src/libsysprof-ui/sysprof-visualizer-row.h         |   83 --
 src/libsysprof-ui/sysprof-visualizer-ticks.c       |   81 +-
 src/libsysprof-ui/sysprof-visualizer-ticks.h       |    5 +-
 src/libsysprof-ui/sysprof-visualizer-view.c        |  762 ------------
 src/libsysprof-ui/sysprof-visualizer-view.h        |   70 --
 src/libsysprof-ui/sysprof-visualizer-view.ui       |   32 -
 src/libsysprof-ui/sysprof-visualizer.c             |  346 ++++++
 src/libsysprof-ui/sysprof-visualizer.h             |   82 ++
 src/libsysprof-ui/sysprof-visualizers-frame.c      |  767 ++++++++++++
 src/libsysprof-ui/sysprof-visualizers-frame.h      |   49 +
 src/libsysprof-ui/sysprof-visualizers-frame.ui     |  419 +++++++
 src/libsysprof-ui/sysprof-zoom-manager.h           |   27 +-
 src/tests/meson.build                              |    3 +-
 src/tests/test-capture-view.c                      |    8 +-
 99 files changed, 10497 insertions(+), 5464 deletions(-)
---
diff --git a/src/libsysprof-ui/css/SysprofDisplay-shared.css b/src/libsysprof-ui/css/SysprofDisplay-shared.css
new file mode 100644
index 0000000..45eddf0
--- /dev/null
+++ b/src/libsysprof-ui/css/SysprofDisplay-shared.css
@@ -0,0 +1,89 @@
+SysprofDisplay dzlmultipaned {
+  -DzlMultiPaned-handle-size: 0;
+  }
+
+SysprofVisualizer {
+  background: @content_view_bg;
+  }
+SysprofVisualizer:not(:last-child) {
+  border-bottom: 1px solid alpha(@borders, 0.3);
+  }
+
+SysprofVisualizerGroup {
+  border-bottom: 1px solid @borders;
+  }
+SysprofVisualizerGroup:last-child {
+  box-shadow: 0 20px 15px 15px alpha(@borders, 0.3);
+  }
+
+SysprofVisualizersFrame box.horizontal.inline-toolbar {
+  padding: 0;
+  margin: 0;
+  border: none;
+  border-radius: 0;
+  border-width: 0;
+  }
+
+SysprofVisualizersFrame scrollbar.horizontal {
+  color: mix(@theme_fg_color, @theme_selected_bg_color, 0.5);
+  background: transparent;
+  }
+
+SysprofVisualizersFrame scrollbar.horizontal contents trough {
+  background: transparent;
+  }
+
+SysprofVisualizersFrame scrollbar.horizontal contents trough slider {
+  margin-left: 1px;
+  margin-right: 1px;
+  padding: 6px;
+  min-height: 14px;
+  background: alpha(@content_view_bg, 0.2);
+  border-radius: 3px;
+  border: 2px solid @theme_selected_bg_color;
+  box-shadow: inset 0 10px 5px alpha(@content_view_bg,.3),
+              inset 0 -15px 5px alpha(@content_view_bg,.1),
+              0px 2px 4px @borders;
+  }
+
+SysprofVisualizerTicks {
+  color: mix(@theme_fg_color, @borders, 0.5);
+  background-color: @theme_bg_color;
+  }
+
+SysprofVisualizersFrame list {
+  background-color: @theme_bg_color;
+  }
+
+SysprofVisualizersFrame list.visualizer-groups row {
+  padding: 0;
+  border-bottom: 1px solid @borders;
+  }
+SysprofVisualizersFrame list.visualizer-groups row:not(:selected) {
+  background-color: @theme_bg_color;
+  }
+SysprofVisualizersFrame list.visualizer-groups row:last-child {
+  box-shadow: 0 20px 15px 15px alpha(@borders, 0.3);
+  }
+
+SysprofVisualizersFrame .left-column .small-button.flat {
+  border-color: transparent;
+  min-height: 8px;
+  min-width: 8px;
+  }
+SysprofVisualizersFrame .left-column .small-button.flat:checked,
+SysprofVisualizersFrame .left-column .small-button.flat:hover {
+  border-color: @borders;
+  }
+
+SysprofDisplay > dzlmultipaned > :nth-child(2) {
+  border-top: 1px solid @borders;
+  }
+
+SysprofVisualizersFrame .selection {
+  border-radius: 3px;
+  background-color: alpha(@theme_selected_bg_color, 0.35);
+  box-shadow: inset 0 10px 5px alpha(@content_view_bg,.3),
+              inset 0 -15px 5px alpha(@content_view_bg,.1),
+              inset 0 0 1px 1px @theme_selected_bg_color;
+  }
diff --git a/src/libsysprof-ui/libsysprof-ui.gresource.xml b/src/libsysprof-ui/libsysprof-ui.gresource.xml
index a4bca35..fd6a597 100644
--- a/src/libsysprof-ui/libsysprof-ui.gresource.xml
+++ b/src/libsysprof-ui/libsysprof-ui.gresource.xml
@@ -2,9 +2,7 @@
 <gresources>
   <gresource prefix="/org/gnome/sysprof">
     <file compressed="true">css/SysprofEnvironEditor-shared.css</file>
-    <file compressed="true">css/SysprofVisualizerView-shared.css</file>
-    <file compressed="true">css/SysprofVisualizerView-Adwaita.css</file>
-    <file compressed="true">css/SysprofVisualizerView-Adwaita-dark.css</file>
+    <file compressed="true">css/SysprofDisplay-shared.css</file>
 
     <!-- Application icons -->
     <file 
alias="icons/scalable/apps/org.gnome.Sysprof.svg">../../data/icons/scalable/apps/org.gnome.Sysprof.svg</file>
@@ -14,20 +12,18 @@
 
   <gresource prefix="/org/gnome/sysprof/ui">
     <file preprocess="xml-stripblanks">sysprof-aid-icon.ui</file>
-    <file preprocess="xml-stripblanks">sysprof-callgraph-view.ui</file>
-    <file preprocess="xml-stripblanks">sysprof-capture-view.ui</file>
+    <file preprocess="xml-stripblanks">sysprof-callgraph-page.ui</file>
+    <file preprocess="xml-stripblanks">sysprof-details-page.ui</file>
     <file preprocess="xml-stripblanks">sysprof-display.ui</file>
-    <file preprocess="xml-stripblanks">sysprof-details-view.ui</file>
-    <file preprocess="xml-stripblanks">sysprof-empty-state-view.ui</file>
     <file preprocess="xml-stripblanks">sysprof-environ-editor-row.ui</file>
     <file preprocess="xml-stripblanks">sysprof-failed-state-view.ui</file>
-    <file preprocess="xml-stripblanks">sysprof-logs-view.ui</file>
-    <file preprocess="xml-stripblanks">sysprof-marks-view.ui</file>
+    <file preprocess="xml-stripblanks">sysprof-logs-page.ui</file>
+    <file preprocess="xml-stripblanks">sysprof-marks-page.ui</file>
     <file preprocess="xml-stripblanks">sysprof-process-model-row.ui</file>
     <file preprocess="xml-stripblanks">sysprof-profiler-assistant.ui</file>
     <file preprocess="xml-stripblanks">sysprof-recording-state-view.ui</file>
     <file preprocess="xml-stripblanks">sysprof-tab.ui</file>
     <file preprocess="xml-stripblanks">sysprof-time-label.ui</file>
-    <file preprocess="xml-stripblanks">sysprof-visualizer-view.ui</file>
+    <file preprocess="xml-stripblanks">sysprof-visualizers-frame.ui</file>
   </gresource>
 </gresources>
diff --git a/src/libsysprof-ui/meson.build b/src/libsysprof-ui/meson.build
index 5a919ac..69ec579 100644
--- a/src/libsysprof-ui/meson.build
+++ b/src/libsysprof-ui/meson.build
@@ -1,84 +1,71 @@
 if get_option('enable_gtk') and get_option('libsysprof')
 
 libsysprof_ui_public_sources = [
-  'sysprof-aid.c',
-  'sysprof-capture-view.c',
-  'sysprof-callgraph-aid.c',
-  'sysprof-callgraph-view.c',
   'sysprof-check.c',
-  'sysprof-color-cycle.c',
-  'sysprof-cpu-aid.c',
-  'sysprof-cpu-visualizer-row.c',
   'sysprof-display.c',
-  'sysprof-empty-state-view.c',
-  'sysprof-failed-state-view.c',
-  'sysprof-line-visualizer-row.c',
-  'sysprof-marks-model.c',
-  'sysprof-marks-view.c',
-  'sysprof-mark-visualizer-row.c',
-  'sysprof-memory-aid.c',
   'sysprof-model-filter.c',
   'sysprof-notebook.c',
+  'sysprof-page.c',
   'sysprof-process-model-row.c',
-  'sysprof-proxy-aid.c',
-  'sysprof-profiler-assistant.c',
-  'sysprof-recording-state-view.c',
-  'sysprof-visualizer-list.c',
-  'sysprof-visualizer-row.c',
-  'sysprof-visualizer-ticks.c',
-  'sysprof-visualizer-view.c',
+  'sysprof-visualizer.c',
+  'sysprof-visualizer-group.c',
   'sysprof-zoom-manager.c',
 ]
 
 libsysprof_ui_private_sources = [
   'pointcache.c',
   'rectangles.c',
+  'sysprof-aid.c',
   'sysprof-aid-icon.c',
-  'sysprof-details-view.c',
+  'sysprof-battery-aid.c',
   'sysprof-cairo.c',
+  'sysprof-callgraph-aid.c',
+  'sysprof-callgraph-page.c',
   'sysprof-cell-renderer-duration.c',
   'sysprof-cell-renderer-percent.c',
-  'sysprof-depth-visualizer-row.c',
-  'sysprof-environ-editor-row.c',
+  'sysprof-color-cycle.c',
+  'sysprof-counters-aid.c',
+  'sysprof-cpu-aid.c',
+  'sysprof-depth-visualizer.c',
+  'sysprof-details-page.c',
+  'sysprof-display.c',
+  'sysprof-environ.c',
   'sysprof-environ-editor.c',
+  'sysprof-environ-editor-row.c',
   'sysprof-environ-variable.c',
-  'sysprof-environ.c',
+  'sysprof-failed-state-view.c',
+  'sysprof-line-visualizer.c',
   'sysprof-log-model.c',
-  'sysprof-logs-view.c',
+  'sysprof-logs-aid.c',
+  'sysprof-logs-page.c',
+  'sysprof-marks-aid.c',
+  'sysprof-marks-model.c',
+  'sysprof-marks-page.c',
+  'sysprof-mark-visualizer.c',
+  'sysprof-memory-aid.c',
+  'sysprof-profiler-assistant.c',
+  'sysprof-proxy-aid.c',
+  'sysprof-recording-state-view.c',
+  'sysprof-scrollmap.c',
   'sysprof-tab.c',
-  'sysprof-time-label.c',
   'sysprof-theme-manager.c',
+  'sysprof-time-label.c',
+  'sysprof-time-visualizer.c',
+  'sysprof-visualizer-group-header.c',
+  'sysprof-visualizers-frame.c',
+  'sysprof-visualizer-ticks.c',
   '../stackstash.c',
 ]
 
 libsysprof_ui_public_headers = [
-  'sysprof-aid.h',
-  'sysprof-capture-view.h',
-  'sysprof-callgraph-aid.h',
-  'sysprof-callgraph-view.h',
-  'sysprof-cell-renderer-percent.h',
   'sysprof-check.h',
-  'sysprof-cpu-aid.h',
-  'sysprof-cpu-visualizer-row.h',
   'sysprof-display.h',
-  'sysprof-empty-state-view.h',
-  'sysprof-failed-state-view.h',
-  'sysprof-line-visualizer-row.h',
-  'sysprof-marks-model.h',
-  'sysprof-marks-view.h',
-  'sysprof-mark-visualizer-row.h',
-  'sysprof-memory-aid.h',
-  'sysprof-model-filter.c',
   'sysprof-model-filter.h',
   'sysprof-notebook.h',
+  'sysprof-page.h',
   'sysprof-process-model-row.h',
-  'sysprof-proxy-aid.h',
-  'sysprof-profiler-assistant.h',
-  'sysprof-recording-state-view.h',
-  'sysprof-visualizer-list.h',
-  'sysprof-visualizer-row.h',
-  'sysprof-visualizer-ticks.h',
-  'sysprof-visualizer-view.h',
+  'sysprof-visualizer.h',
+  'sysprof-visualizer-group.h',
   'sysprof-zoom-manager.h',
   'sysprof-ui.h',
 ]
diff --git a/src/libsysprof-ui/rectangles.c b/src/libsysprof-ui/rectangles.c
index 1623502..2ac8ca0 100644
--- a/src/libsysprof-ui/rectangles.c
+++ b/src/libsysprof-ui/rectangles.c
@@ -20,7 +20,7 @@
 
 #include "rectangles.h"
 #include "sysprof-color-cycle.h"
-#include "sysprof-visualizer-row.h"
+#include "sysprof-visualizer.h"
 
 typedef struct
 {
@@ -153,18 +153,18 @@ rectangles_draw (Rectangles *self,
 {
   GtkAllocation alloc;
   gdouble range;
-  guint n_rows;
+  guint ns;
 
   g_assert (self != NULL);
-  g_assert (SYSPROF_IS_VISUALIZER_ROW (row));
+  g_assert (SYSPROF_IS_VISUALIZER (row));
   g_assert (cr != NULL);
 
   if (!self->sorted)
     rectangles_sort (self);
 
   gtk_widget_get_allocation (row, &alloc);
-  n_rows = g_hash_table_size (self->y_indexes);
-  if (n_rows == 0 || alloc.height == 0)
+  ns = g_hash_table_size (self->y_indexes);
+  if (ns == 0 || alloc.height == 0)
     return;
 
   range = self->end_time - self->begin_time;
@@ -173,24 +173,24 @@ rectangles_draw (Rectangles *self,
     {
       Rectangle *rect = &g_array_index (self->rectangles, Rectangle, i);
       guint y_index = GPOINTER_TO_UINT (g_hash_table_lookup (self->y_indexes, rect->name));
-      SysprofVisualizerRowRelativePoint in_points[2];
-      SysprofVisualizerRowAbsolutePoint out_points[2];
+      SysprofVisualizerRelativePoint in_points[2];
+      SysprofVisualizerAbsolutePoint out_points[2];
       GdkRectangle r;
       GdkRGBA *rgba;
 
       g_assert (y_index > 0);
-      g_assert (y_index <= n_rows);
+      g_assert (y_index <= ns);
 
       in_points[0].x = (rect->begin - self->begin_time) / range;
-      in_points[0].y = (y_index - 1) / (gdouble)n_rows;
+      in_points[0].y = (y_index - 1) / (gdouble)ns;
       in_points[1].x = (rect->end - self->begin_time) / range;
       in_points[1].y = 0;
 
-      sysprof_visualizer_row_translate_points (SYSPROF_VISUALIZER_ROW (row),
+      sysprof_visualizer_translate_points (SYSPROF_VISUALIZER (row),
                                                in_points, G_N_ELEMENTS (in_points),
                                                out_points, G_N_ELEMENTS (out_points));
 
-      r.height = alloc.height / (gdouble)n_rows;
+      r.height = alloc.height / (gdouble)ns;
       r.x = out_points[0].x;
       r.y = out_points[0].y - r.height;
 
diff --git a/src/libsysprof-ui/sysprof-aid.c b/src/libsysprof-ui/sysprof-aid.c
index c1461f2..1bf3a48 100644
--- a/src/libsysprof-ui/sysprof-aid.c
+++ b/src/libsysprof-ui/sysprof-aid.c
@@ -49,6 +49,29 @@ enum {
 
 static GParamSpec *properties [N_PROPS];
 
+static void
+sysprof_aid_real_present_async (SysprofAid           *self,
+                                SysprofCaptureReader *reader,
+                                SysprofDisplay       *display,
+                                GCancellable         *cancellable,
+                                GAsyncReadyCallback   callback,
+                                gpointer              user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           sysprof_aid_real_present_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Not supported");
+}
+
+static gboolean
+sysprof_aid_real_present_finish (SysprofAid    *self,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
 static void
 sysprof_aid_finalize (GObject *object)
 {
@@ -121,6 +144,9 @@ sysprof_aid_class_init (SysprofAidClass *klass)
   object_class->get_property = sysprof_aid_get_property;
   object_class->set_property = sysprof_aid_set_property;
 
+  klass->present_async = sysprof_aid_real_present_async;
+  klass->present_finish = sysprof_aid_real_present_finish;
+
   properties [PROP_DISPLAY_NAME] =
     g_param_spec_string ("display-name",
                          "Display Name",
@@ -146,7 +172,7 @@ sysprof_aid_class_init (SysprofAidClass *klass)
 }
 
 static void
-sysprof_aid_init (SysprofAid *self)
+sysprof_aid_init (SysprofAid *self G_GNUC_UNUSED)
 {
 }
 
@@ -258,10 +284,10 @@ sysprof_aid_prepare (SysprofAid      *self,
 }
 
 static void
-sysprof_aid_add_child (GtkBuildable *buildable,
-                       GtkBuilder   *builder,
-                       GObject      *object,
-                       const gchar  *type)
+sysprof_aid_add_child (GtkBuildable       *buildable,
+                       GtkBuilder         *builder,
+                       GObject            *object,
+                       const gchar        *type G_GNUC_UNUSED)
 {
   SysprofAid *self = (SysprofAid *)buildable;
   SysprofAidPrivate *priv = sysprof_aid_get_instance_private (self);
@@ -298,3 +324,30 @@ sysprof_aid_new (const gchar *display_name,
                        "icon-name", icon_name,
                        NULL);
 }
+
+void
+sysprof_aid_present_async (SysprofAid           *self,
+                           SysprofCaptureReader *reader,
+                           SysprofDisplay       *display,
+                           GCancellable         *cancellable,
+                           GAsyncReadyCallback   callback,
+                           gpointer              user_data)
+{
+  g_return_if_fail (SYSPROF_IS_AID (self));
+  g_return_if_fail (reader != NULL);
+  g_return_if_fail (SYSPROF_IS_DISPLAY (display));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  SYSPROF_AID_GET_CLASS (self)->present_async (self, reader, display, cancellable, callback, user_data);
+}
+
+gboolean
+sysprof_aid_present_finish (SysprofAid    *self,
+                            GAsyncResult  *result,
+                            GError       **error)
+{
+  g_return_val_if_fail (SYSPROF_IS_AID (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return SYSPROF_AID_GET_CLASS (self)->present_finish (self, result, error);
+}
diff --git a/src/libsysprof-ui/sysprof-aid.h b/src/libsysprof-ui/sysprof-aid.h
index ba8dfe3..70a1666 100644
--- a/src/libsysprof-ui/sysprof-aid.h
+++ b/src/libsysprof-ui/sysprof-aid.h
@@ -20,49 +20,57 @@
 
 #pragma once
 
-#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
-# error "Only <sysprof-ui.h> can be included directly."
-#endif
-
 #include <gio/gio.h>
 #include <sysprof.h>
 
+#include "sysprof-display.h"
+
 G_BEGIN_DECLS
 
 #define SYSPROF_TYPE_AID (sysprof_aid_get_type())
 
-SYSPROF_AVAILABLE_IN_ALL
 G_DECLARE_DERIVABLE_TYPE (SysprofAid, sysprof_aid, SYSPROF, AID, GObject)
 
 struct _SysprofAidClass
 {
   GObjectClass parent_class;
 
-  void (*prepare) (SysprofAid      *self,
-                   SysprofProfiler *profiler);
+  void     (*prepare)        (SysprofAid           *self,
+                              SysprofProfiler      *profiler);
+  void     (*present_async)  (SysprofAid           *self,
+                              SysprofCaptureReader *reader,
+                              SysprofDisplay       *display,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data);
+  gboolean (*present_finish) (SysprofAid           *self,
+                              GAsyncResult         *result,
+                              GError              **error);
 
-  /*< gpointer >*/
+  /*< private >*/
   gpointer _reserved[16];
 };
 
-SYSPROF_AVAILABLE_IN_ALL
-SysprofAid  *sysprof_aid_new              (const gchar     *display_name,
-                                           const gchar     *icon_name);
-SYSPROF_AVAILABLE_IN_ALL
-const gchar *sysprof_aid_get_display_name (SysprofAid      *self);
-SYSPROF_AVAILABLE_IN_ALL
-void         sysprof_aid_set_display_name (SysprofAid      *self,
-                                           const gchar     *display_name);
-SYSPROF_AVAILABLE_IN_ALL
-GIcon       *sysprof_aid_get_icon         (SysprofAid      *self);
-SYSPROF_AVAILABLE_IN_ALL
-void         sysprof_aid_set_icon         (SysprofAid      *self,
-                                           GIcon           *icon);
-SYSPROF_AVAILABLE_IN_ALL
-void         sysprof_aid_set_icon_name    (SysprofAid      *self,
-                                           const gchar     *icon_name);
-SYSPROF_AVAILABLE_IN_ALL
-void         sysprof_aid_prepare          (SysprofAid      *self,
-                                           SysprofProfiler *profiler);
+SysprofAid  *sysprof_aid_new              (const gchar          *display_name,
+                                           const gchar          *icon_name);
+const gchar *sysprof_aid_get_display_name (SysprofAid           *self);
+void         sysprof_aid_set_display_name (SysprofAid           *self,
+                                           const gchar          *display_name);
+GIcon       *sysprof_aid_get_icon         (SysprofAid           *self);
+void         sysprof_aid_set_icon         (SysprofAid           *self,
+                                           GIcon                *icon);
+void         sysprof_aid_set_icon_name    (SysprofAid           *self,
+                                           const gchar          *icon_name);
+void         sysprof_aid_prepare          (SysprofAid           *self,
+                                           SysprofProfiler      *profiler);
+void         sysprof_aid_present_async    (SysprofAid           *self,
+                                           SysprofCaptureReader *reader,
+                                           SysprofDisplay       *display,
+                                           GCancellable         *cancellable,
+                                           GAsyncReadyCallback   callback,
+                                           gpointer              user_data);
+gboolean     sysprof_aid_present_finish   (SysprofAid           *self,
+                                           GAsyncResult         *result,
+                                           GError              **error);
 
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-battery-aid.c b/src/libsysprof-ui/sysprof-battery-aid.c
new file mode 100644
index 0000000..5675030
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-battery-aid.c
@@ -0,0 +1,239 @@
+/* sysprof-battery-aid.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-battery-aid"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "sysprof-color-cycle.h"
+#include "sysprof-battery-aid.h"
+#include "sysprof-line-visualizer.h"
+
+struct _SysprofBatteryAid
+{
+  SysprofAid parent_instance;
+};
+
+typedef struct
+{
+  SysprofCaptureCursor *cursor;
+  SysprofDisplay       *display;
+} Present;
+
+G_DEFINE_TYPE (SysprofBatteryAid, sysprof_battery_aid, SYSPROF_TYPE_AID)
+
+static void
+present_free (gpointer data)
+{
+  Present *p = data;
+
+  g_clear_pointer (&p->cursor, sysprof_capture_cursor_unref);
+  g_clear_object (&p->display);
+  g_slice_free (Present, p);
+}
+
+/**
+ * sysprof_battery_aid_new:
+ *
+ * Create a new #SysprofBatteryAid.
+ *
+ * Returns: (transfer full): a newly created #SysprofBatteryAid
+ *
+ * Since: 3.34
+ */
+SysprofAid *
+sysprof_battery_aid_new (void)
+{
+  return g_object_new (SYSPROF_TYPE_BATTERY_AID, NULL);
+}
+
+static void
+sysprof_battery_aid_prepare (SysprofAid      *self,
+                         SysprofProfiler *profiler)
+{
+#ifdef __linux__
+  g_autoptr(SysprofSource) source = NULL;
+
+  g_assert (SYSPROF_IS_BATTERY_AID (self));
+  g_assert (SYSPROF_IS_PROFILER (profiler));
+
+  source = sysprof_battery_source_new ();
+  sysprof_profiler_add_source (profiler, source);
+#endif
+}
+
+static gboolean
+collect_battery_counters (const SysprofCaptureFrame *frame,
+                          gpointer                   user_data)
+{
+  SysprofCaptureCounterDefine *def = (SysprofCaptureCounterDefine *)frame;
+  GArray *counters = user_data;
+
+  g_assert (frame != NULL);
+  g_assert (frame->type == SYSPROF_CAPTURE_FRAME_CTRDEF);
+  g_assert (counters != NULL);
+
+  for (guint i = 0; i < def->n_counters; i++)
+    {
+      const SysprofCaptureCounter *counter = &def->counters[i];
+
+      if (g_strcmp0 (counter->category, "Battery Charge") == 0)
+        g_array_append_vals (counters, counter, 1);
+    }
+
+  return TRUE;
+}
+
+static void
+sysprof_battery_aid_present_worker (GTask        *task,
+                                    gpointer      source_object,
+                                    gpointer      task_data,
+                                    GCancellable *cancellable)
+{
+  Present *present = task_data;
+  g_autoptr(GArray) counters = NULL;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (SYSPROF_IS_BATTERY_AID (source_object));
+  g_assert (present != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  counters = g_array_new (FALSE, FALSE, sizeof (SysprofCaptureCounter));
+  sysprof_capture_cursor_foreach (present->cursor, collect_battery_counters, counters);
+  g_task_return_pointer (task,
+                         g_steal_pointer (&counters),
+                         (GDestroyNotify) g_array_unref);
+}
+
+static void
+sysprof_battery_aid_present_async (SysprofAid           *aid,
+                                   SysprofCaptureReader *reader,
+                                   SysprofDisplay       *display,
+                                   GCancellable         *cancellable,
+                                   GAsyncReadyCallback   callback,
+                                   gpointer              user_data)
+{
+  static const SysprofCaptureFrameType types[] = { SYSPROF_CAPTURE_FRAME_CTRDEF };
+  g_autoptr(SysprofCaptureCondition) condition = NULL;
+  g_autoptr(SysprofCaptureCursor) cursor = NULL;
+  g_autoptr(GTask) task = NULL;
+  Present present;
+
+  g_assert (SYSPROF_IS_BATTERY_AID (aid));
+  g_assert (reader != NULL);
+  g_assert (SYSPROF_IS_DISPLAY (display));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  condition = sysprof_capture_condition_new_where_type_in (1, types);
+  cursor = sysprof_capture_cursor_new (reader);
+  sysprof_capture_cursor_add_condition (cursor, g_steal_pointer (&condition));
+
+  present.cursor = g_steal_pointer (&cursor);
+  present.display = g_object_ref (display);
+
+  task = g_task_new (aid, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_battery_aid_present_async);
+  g_task_set_task_data (task,
+                        g_slice_dup (Present, &present),
+                        present_free);
+  g_task_run_in_thread (task, sysprof_battery_aid_present_worker);
+}
+
+static gboolean
+sysprof_battery_aid_present_finish (SysprofAid    *aid,
+                                    GAsyncResult  *result,
+                                    GError       **error)
+{
+  g_autoptr(GArray) counters = NULL;
+  Present *present;
+
+  g_assert (SYSPROF_IS_AID (aid));
+  g_assert (G_IS_TASK (result));
+
+  present = g_task_get_task_data (G_TASK (result));
+
+  if ((counters = g_task_propagate_pointer (G_TASK (result), error)))
+    {
+      g_autoptr(SysprofColorCycle) cycle = sysprof_color_cycle_new ();
+      SysprofVisualizerGroup *group;
+
+      group = g_object_new (SYSPROF_TYPE_VISUALIZER_GROUP,
+                            "can-focus", TRUE,
+                            "title", _("Battery Charge"),
+                            "visible", TRUE,
+                            NULL);
+
+      for (guint i = 0; i < counters->len; i++)
+        {
+          const SysprofCaptureCounter *ctr = &g_array_index (counters, SysprofCaptureCounter, i);
+
+          if (g_strcmp0 (ctr->category, "Battery Charge") == 0)
+            {
+              g_autofree gchar *title = NULL;
+              gboolean is_combined = g_str_equal (ctr->name, "Combined");
+              GtkWidget *row;
+              GdkRGBA rgba;
+
+              if (is_combined)
+                title = g_strdup (_("Battery Charge (All)"));
+              else
+                title = g_strdup_printf ("Battery Charge (%s)", ctr->name);
+
+              row = g_object_new (SYSPROF_TYPE_LINE_VISUALIZER,
+                                  "title", title,
+                                  "height-request", 35,
+                                  "visible", is_combined,
+                                  NULL);
+              sysprof_color_cycle_next (cycle, &rgba);
+              sysprof_line_visualizer_add_counter (SYSPROF_LINE_VISUALIZER (row), ctr->id, &rgba);
+              sysprof_visualizer_group_insert (group,
+                                               SYSPROF_VISUALIZER (row),
+                                               is_combined ? 0 : -1,
+                                               !is_combined);
+            }
+        }
+
+      if (counters->len > 0)
+        sysprof_display_add_group (present->display, group);
+      else
+        gtk_widget_destroy (GTK_WIDGET (group));
+    }
+
+  return counters != NULL;
+}
+
+static void
+sysprof_battery_aid_class_init (SysprofBatteryAidClass *klass)
+{
+  SysprofAidClass *aid_class = SYSPROF_AID_CLASS (klass);
+
+  aid_class->prepare = sysprof_battery_aid_prepare;
+  aid_class->present_async = sysprof_battery_aid_present_async;
+  aid_class->present_finish = sysprof_battery_aid_present_finish;
+}
+
+static void
+sysprof_battery_aid_init (SysprofBatteryAid *self)
+{
+  sysprof_aid_set_display_name (SYSPROF_AID (self), _("Battery"));
+  sysprof_aid_set_icon_name (SYSPROF_AID (self), "battery-low-charging-symbolic");
+}
diff --git a/src/libsysprof-ui/sysprof-depth-visualizer-row.h b/src/libsysprof-ui/sysprof-battery-aid.h
similarity index 73%
rename from src/libsysprof-ui/sysprof-depth-visualizer-row.h
rename to src/libsysprof-ui/sysprof-battery-aid.h
index f413f8d..563793f 100644
--- a/src/libsysprof-ui/sysprof-depth-visualizer-row.h
+++ b/src/libsysprof-ui/sysprof-battery-aid.h
@@ -1,4 +1,4 @@
-/* sysprof-depth-visualizer-row.h
+/* sysprof-battery-aid.h
  *
  * Copyright 2019 Christian Hergert <chergert redhat com>
  *
@@ -20,12 +20,14 @@
 
 #pragma once
 
-#include "sysprof-visualizer-row.h"
+#include "sysprof-aid.h"
 
 G_BEGIN_DECLS
 
-#define SYSPROF_TYPE_DEPTH_VISUALIZER_ROW (sysprof_depth_visualizer_row_get_type())
+#define SYSPROF_TYPE_BATTERY_AID (sysprof_battery_aid_get_type())
 
-G_DECLARE_FINAL_TYPE (SysprofDepthVisualizerRow, sysprof_depth_visualizer_row, SYSPROF, 
DEPTH_VISUALIZER_ROW, SysprofVisualizerRow)
+G_DECLARE_FINAL_TYPE (SysprofBatteryAid, sysprof_battery_aid, SYSPROF, BATTERY_AID, SysprofAid)
+
+SysprofAid *sysprof_battery_aid_new (void);
 
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-callgraph-aid.c b/src/libsysprof-ui/sysprof-callgraph-aid.c
index 9e3c45a..5b0ebb0 100644
--- a/src/libsysprof-ui/sysprof-callgraph-aid.c
+++ b/src/libsysprof-ui/sysprof-callgraph-aid.c
@@ -25,14 +25,46 @@
 #include <glib/gi18n.h>
 
 #include "sysprof-callgraph-aid.h"
+#include "sysprof-callgraph-page.h"
+#include "sysprof-depth-visualizer.h"
 
 struct _SysprofCallgraphAid
 {
   SysprofAid parent_instance;
 };
 
+typedef struct
+{
+  SysprofCaptureCursor *cursor;
+  SysprofDisplay *display;
+  guint has_samples : 1;
+} Present;
+
 G_DEFINE_TYPE (SysprofCallgraphAid, sysprof_callgraph_aid, SYSPROF_TYPE_AID)
 
+static void
+present_free (gpointer data)
+{
+  Present *p = data;
+
+  g_clear_pointer (&p->cursor, sysprof_capture_cursor_unref);
+  g_clear_object (&p->display);
+  g_slice_free (Present, p);
+}
+
+static void
+on_group_activated_cb (SysprofVisualizerGroup *group,
+                       SysprofPage            *page)
+{
+  SysprofDisplay *display;
+
+  g_assert (SYSPROF_IS_VISUALIZER_GROUP (group));
+  g_assert (SYSPROF_IS_PAGE (page));
+
+  display = SYSPROF_DISPLAY (gtk_widget_get_ancestor (GTK_WIDGET (page), SYSPROF_TYPE_DISPLAY));
+  sysprof_display_set_visible_page (display, page);
+}
+
 /**
  * sysprof_callgraph_aid_new:
  *
@@ -82,12 +114,157 @@ sysprof_callgraph_aid_prepare (SysprofAid      *self,
 #endif
 }
 
+static gboolean
+discover_samples_cb (const SysprofCaptureFrame *frame,
+                     gpointer                   user_data)
+{
+  Present *p = user_data;
+
+  g_assert (frame != NULL);
+  g_assert (p != NULL);
+
+  if (frame->type == SYSPROF_CAPTURE_FRAME_SAMPLE)
+    {
+      p->has_samples = TRUE;
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+static void
+sysprof_callgraph_aid_present_worker (GTask        *task,
+                                      gpointer      source_object,
+                                      gpointer      task_data,
+                                      GCancellable *cancellable)
+{
+  Present *p = task_data;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (SYSPROF_IS_CALLGRAPH_AID (source_object));
+  g_assert (p != NULL);
+  g_assert (p->cursor != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /* If we find a sample frame, then we should enable the callgraph
+   * and stack visualizers.
+   */
+  sysprof_capture_cursor_foreach (p->cursor, discover_samples_cb, p);
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+sysprof_callgraph_aid_present_async (SysprofAid           *aid,
+                                     SysprofCaptureReader *reader,
+                                     SysprofDisplay       *display,
+                                     GCancellable         *cancellable,
+                                     GAsyncReadyCallback   callback,
+                                     gpointer              user_data)
+{
+  static const SysprofCaptureFrameType types[] = { SYSPROF_CAPTURE_FRAME_SAMPLE };
+  g_autoptr(SysprofCaptureCondition) condition = NULL;
+  g_autoptr(SysprofCaptureCursor) cursor = NULL;
+  g_autoptr(GTask) task = NULL;
+  Present present;
+
+  g_assert (SYSPROF_IS_CALLGRAPH_AID (aid));
+  g_assert (reader != NULL);
+  g_assert (SYSPROF_IS_DISPLAY (display));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  condition = sysprof_capture_condition_new_where_type_in (1, types);
+  cursor = sysprof_capture_cursor_new (reader);
+  sysprof_capture_cursor_add_condition (cursor, g_steal_pointer (&condition));
+
+  present.cursor = g_steal_pointer (&cursor);
+  present.display = g_object_ref (display);
+
+  task = g_task_new (aid, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_callgraph_aid_present_async);
+  g_task_set_task_data (task,
+                        g_slice_dup (Present, &present),
+                        present_free);
+  g_task_run_in_thread (task, sysprof_callgraph_aid_present_worker);
+}
+
+static gboolean
+sysprof_callgraph_aid_present_finish (SysprofAid    *aid,
+                                      GAsyncResult  *result,
+                                      GError       **error)
+{
+  Present *p;
+
+  g_assert (SYSPROF_IS_CALLGRAPH_AID (aid));
+  g_assert (G_IS_TASK (result));
+
+  p = g_task_get_task_data (G_TASK (result));
+
+  if (p->has_samples)
+    {
+      SysprofVisualizerGroup *group;
+      SysprofVisualizer *depth;
+      SysprofPage *page;
+
+      group = g_object_new (SYSPROF_TYPE_VISUALIZER_GROUP,
+                            "can-focus", TRUE,
+                            "has-page", TRUE,
+                            "priority", -500,
+                            "title", _("Stack Traces"),
+                            "visible", TRUE,
+                            NULL);
+
+      depth = sysprof_depth_visualizer_new (SYSPROF_DEPTH_VISUALIZER_COMBINED);
+      g_object_set (depth,
+                    "title", _("Stack Traces"),
+                    "height-request", 35,
+                    "visible", TRUE,
+                    NULL);
+      sysprof_visualizer_group_insert (group, depth, 0, FALSE);
+
+      depth = sysprof_depth_visualizer_new (SYSPROF_DEPTH_VISUALIZER_KERNEL_ONLY);
+      g_object_set (depth,
+                    "title", _("Stack Traces (In Kernel)"),
+                    "height-request", 35,
+                    "visible", FALSE,
+                    NULL);
+      sysprof_visualizer_group_insert (group, depth, 1, TRUE);
+
+      depth = sysprof_depth_visualizer_new (SYSPROF_DEPTH_VISUALIZER_USER_ONLY);
+      g_object_set (depth,
+                    "title", _("Stack Traces (In User)"),
+                    "height-request", 35,
+                    "visible", FALSE,
+                    NULL);
+      sysprof_visualizer_group_insert (group, depth, 2, TRUE);
+
+      sysprof_display_add_group (p->display, group);
+
+      page = g_object_new (SYSPROF_TYPE_CALLGRAPH_PAGE,
+                           "title", _("Callgraph"),
+                           "vexpand", TRUE,
+                           "visible", TRUE,
+                           NULL);
+      sysprof_display_add_page (p->display, page);
+      sysprof_display_set_visible_page (p->display, page);
+
+      g_signal_connect_object (group,
+                               "group-activated",
+                               G_CALLBACK (on_group_activated_cb),
+                               page,
+                               0);
+    }
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
 static void
 sysprof_callgraph_aid_class_init (SysprofCallgraphAidClass *klass)
 {
   SysprofAidClass *aid_class = SYSPROF_AID_CLASS (klass);
 
   aid_class->prepare = sysprof_callgraph_aid_prepare;
+  aid_class->present_async = sysprof_callgraph_aid_present_async;
+  aid_class->present_finish = sysprof_callgraph_aid_present_finish;
 }
 
 static void
diff --git a/src/libsysprof-ui/sysprof-callgraph-aid.h b/src/libsysprof-ui/sysprof-callgraph-aid.h
index ecfe053..3fdde80 100644
--- a/src/libsysprof-ui/sysprof-callgraph-aid.h
+++ b/src/libsysprof-ui/sysprof-callgraph-aid.h
@@ -26,10 +26,8 @@ G_BEGIN_DECLS
 
 #define SYSPROF_TYPE_CALLGRAPH_AID (sysprof_callgraph_aid_get_type())
 
-SYSPROF_AVAILABLE_IN_ALL
 G_DECLARE_FINAL_TYPE (SysprofCallgraphAid, sysprof_callgraph_aid, SYSPROF, CALLGRAPH_AID, SysprofAid)
 
-SYSPROF_AVAILABLE_IN_ALL
 SysprofAid *sysprof_callgraph_aid_new (void);
 
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-callgraph-page.c b/src/libsysprof-ui/sysprof-callgraph-page.c
new file mode 100644
index 0000000..354cdf1
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-callgraph-page.c
@@ -0,0 +1,1293 @@
+/* sysprof-callgraph-page.c
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/* Sysprof -- Sampling, systemwide CPU profiler
+ * Copyright 2004, Red Hat, Inc.
+ * Copyright 2004, 2005, 2006, Soeren Sandmann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "../stackstash.h"
+
+#include "sysprof-callgraph-page.h"
+#include "sysprof-cell-renderer-percent.h"
+
+typedef struct
+{
+  SysprofCallgraphProfile  *profile;
+
+  GtkTreeView              *callers_view;
+  GtkTreeView              *functions_view;
+  GtkTreeView              *descendants_view;
+  GtkTreeViewColumn        *descendants_name_column;
+  GtkStack                 *stack;
+
+  GQueue                   *history;
+
+  guint                     profile_size;
+  guint                     loading;
+} SysprofCallgraphPagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (SysprofCallgraphPage, sysprof_callgraph_page, SYSPROF_TYPE_PAGE)
+
+enum {
+  PROP_0,
+  PROP_PROFILE,
+  N_PROPS
+};
+
+enum {
+  GO_PREVIOUS,
+  N_SIGNALS
+};
+
+enum {
+  COLUMN_NAME,
+  COLUMN_SELF,
+  COLUMN_TOTAL,
+  COLUMN_POINTER,
+  COLUMN_HITS,
+};
+
+static void sysprof_callgraph_page_update_descendants (SysprofCallgraphPage *self,
+                                                       StackNode            *node);
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static guint
+sysprof_callgraph_page_get_profile_size (SysprofCallgraphPage *self)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+  StackStash *stash;
+  StackNode *node;
+  guint size = 0;
+
+  g_assert (SYSPROF_IS_CALLGRAPH_PAGE (self));
+
+  if (priv->profile_size != 0)
+    return priv->profile_size;
+
+  if (priv->profile == NULL)
+    return 0;
+
+  if (NULL == (stash = sysprof_callgraph_profile_get_stash (priv->profile)))
+    return 0;
+
+  for (node = stack_stash_get_root (stash); node != NULL; node = node->siblings)
+    size += node->total;
+
+  priv->profile_size = size;
+
+  return size;
+}
+
+static void
+build_functions_store (StackNode *node,
+                       gpointer   user_data)
+{
+  struct {
+    GtkListStore *store;
+    gdouble profile_size;
+  } *state = user_data;
+  GtkTreeIter iter;
+  const StackNode *n;
+  guint size = 0;
+  guint total = 0;
+
+  g_assert (state != NULL);
+  g_assert (GTK_IS_LIST_STORE (state->store));
+
+  for (n = node; n != NULL; n = n->next)
+    {
+      size += n->size;
+      if (n->toplevel)
+        total += n->total;
+    }
+
+  gtk_list_store_append (state->store, &iter);
+  gtk_list_store_set (state->store, &iter,
+                      COLUMN_NAME, U64_TO_POINTER(node->data),
+                      COLUMN_SELF, 100.0 * size / state->profile_size,
+                      COLUMN_TOTAL, 100.0 * total / state->profile_size,
+                      COLUMN_POINTER, node,
+                      -1);
+
+}
+
+static void
+sysprof_callgraph_page_load (SysprofCallgraphPage    *self,
+                             SysprofCallgraphProfile *profile)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+  GtkListStore *functions;
+  StackStash *stash;
+  StackNode *n;
+  GtkTreeIter iter;
+  struct {
+    GtkListStore *store;
+    gdouble profile_size;
+  } state = { 0 };
+
+  g_assert (SYSPROF_IS_CALLGRAPH_PAGE (self));
+  g_assert (SYSPROF_IS_CALLGRAPH_PROFILE (profile));
+
+  /*
+   * TODO: This is probably the type of thing we want to do off the main
+   *       thread. We should be able to build the tree models off thread
+   *       and then simply apply them on the main thread.
+   *
+   *       In the mean time, we should set the state of the widget to
+   *       insensitive and give some indication of loading progress.
+   */
+
+  if (!g_set_object (&priv->profile, profile))
+    return;
+
+  if (sysprof_callgraph_profile_is_empty (profile))
+    return;
+
+  stash = sysprof_callgraph_profile_get_stash (profile);
+
+  for (n = stack_stash_get_root (stash); n; n = n->siblings)
+    state.profile_size += n->total;
+
+  functions = gtk_list_store_new (4, G_TYPE_STRING, G_TYPE_DOUBLE, G_TYPE_DOUBLE, G_TYPE_POINTER);
+
+  state.store = functions;
+  stack_stash_foreach_by_address (stash, build_functions_store, &state);
+
+  gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (functions),
+                                        COLUMN_TOTAL,
+                                        GTK_SORT_DESCENDING);
+
+  gtk_tree_view_set_model (priv->functions_view, GTK_TREE_MODEL (functions));
+  gtk_tree_view_set_model (priv->callers_view, NULL);
+  gtk_tree_view_set_model (priv->descendants_view, NULL);
+
+  if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (functions), &iter))
+    {
+      GtkTreeSelection *selection;
+
+      selection = gtk_tree_view_get_selection (priv->functions_view);
+      gtk_tree_selection_select_iter (selection, &iter);
+    }
+
+  gtk_stack_set_visible_child_name (priv->stack, "callgraph");
+
+  g_clear_object (&functions);
+}
+
+void
+_sysprof_callgraph_page_set_failed (SysprofCallgraphPage *self)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_CALLGRAPH_PAGE (self));
+
+  gtk_stack_set_visible_child_name (priv->stack, "empty-state");
+}
+
+static void
+sysprof_callgraph_page_unload (SysprofCallgraphPage *self)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+
+  g_assert (SYSPROF_IS_CALLGRAPH_PAGE (self));
+  g_assert (SYSPROF_IS_CALLGRAPH_PROFILE (priv->profile));
+
+  g_queue_clear (priv->history);
+  g_clear_object (&priv->profile);
+  priv->profile_size = 0;
+
+  gtk_tree_view_set_model (priv->callers_view, NULL);
+  gtk_tree_view_set_model (priv->functions_view, NULL);
+  gtk_tree_view_set_model (priv->descendants_view, NULL);
+
+  gtk_stack_set_visible_child_name (priv->stack, "empty-state");
+}
+
+/**
+ * sysprof_callgraph_page_get_profile:
+ *
+ * Returns: (transfer none): An #SysprofCallgraphProfile.
+ */
+SysprofCallgraphProfile *
+sysprof_callgraph_page_get_profile (SysprofCallgraphPage *self)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+
+  g_return_val_if_fail (SYSPROF_IS_CALLGRAPH_PAGE (self), NULL);
+
+  return priv->profile;
+}
+
+void
+sysprof_callgraph_page_set_profile (SysprofCallgraphPage    *self,
+                                    SysprofCallgraphProfile *profile)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_CALLGRAPH_PAGE (self));
+  g_return_if_fail (!profile || SYSPROF_IS_CALLGRAPH_PROFILE (profile));
+
+  if (profile != priv->profile)
+    {
+      if (priv->profile)
+        sysprof_callgraph_page_unload (self);
+
+      if (profile)
+        sysprof_callgraph_page_load (self, profile);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROFILE]);
+    }
+}
+
+static void
+sysprof_callgraph_page_expand_descendants (SysprofCallgraphPage *self)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+  GtkTreeModel *model;
+  GList *all_paths = NULL;
+  GtkTreePath *first_path;
+  GtkTreeIter iter;
+  gdouble top_value = 0;
+  gint max_rows = 40; /* FIXME */
+  gint n_rows;
+
+  g_assert (SYSPROF_IS_CALLGRAPH_PAGE (self));
+
+  model = gtk_tree_view_get_model (priv->descendants_view);
+  first_path = gtk_tree_path_new_first ();
+  all_paths = g_list_prepend (all_paths, first_path);
+  n_rows = 1;
+
+  gtk_tree_model_get_iter (model, &iter, first_path);
+  gtk_tree_model_get (model, &iter,
+                      COLUMN_TOTAL, &top_value,
+                      -1);
+
+  while ((all_paths != NULL) && (n_rows < max_rows))
+    {
+      GtkTreeIter best_iter;
+      GtkTreePath *best_path = NULL;
+      GList *list;
+      gdouble best_value = 0.0;
+      gint n_children;
+      gint i;
+
+      for (list = all_paths; list != NULL; list = list->next)
+        {
+          GtkTreePath *path = list->data;
+
+          g_assert (path != NULL);
+
+          if (gtk_tree_model_get_iter (model, &iter, path))
+            {
+              gdouble value;
+
+              gtk_tree_model_get (model, &iter,
+                                  COLUMN_TOTAL, &value,
+                                  -1);
+
+              if (value >= best_value)
+                {
+                  best_value = value;
+                  best_path = path;
+                  best_iter = iter;
+                }
+            }
+        }
+
+      n_children = gtk_tree_model_iter_n_children (model, &best_iter);
+
+      if ((n_children > 0) &&
+          ((best_value / top_value) > 0.04) &&
+          ((n_children + gtk_tree_path_get_depth (best_path)) / (gdouble)max_rows) < (best_value / 
top_value))
+        {
+          gtk_tree_view_expand_row (priv->descendants_view, best_path, FALSE);
+          n_rows += n_children;
+
+          if (gtk_tree_path_get_depth (best_path) < 4)
+            {
+              GtkTreePath *path;
+
+              path = gtk_tree_path_copy (best_path);
+              gtk_tree_path_down (path);
+
+              for (i = 0; i < n_children; i++)
+                {
+                  all_paths = g_list_prepend (all_paths, path);
+
+                  path = gtk_tree_path_copy (path);
+                  gtk_tree_path_next (path);
+                }
+
+              gtk_tree_path_free (path);
+            }
+        }
+
+      all_paths = g_list_remove (all_paths, best_path);
+
+      /* Always expand at least once */
+      if ((all_paths == NULL) && (n_rows == 1))
+        gtk_tree_view_expand_row (priv->descendants_view, best_path, FALSE);
+
+      gtk_tree_path_free (best_path);
+    }
+
+  g_list_free_full (all_paths, (GDestroyNotify)gtk_tree_path_free);
+}
+
+typedef struct
+{
+  StackNode   *node;
+  const gchar *name;
+  guint        self;
+  guint        total;
+} Caller;
+
+static Caller *
+caller_new (StackNode *node)
+{
+  Caller *c;
+
+  c = g_slice_new (Caller);
+  c->name = U64_TO_POINTER (node->data);
+  c->self = 0;
+  c->total = 0;
+  c->node = node;
+
+  return c;
+}
+
+static void
+caller_free (gpointer data)
+{
+  Caller *c = data;
+  g_slice_free (Caller, c);
+}
+
+static void
+sysprof_callgraph_page_function_selection_changed (SysprofCallgraphPage *self,
+                                                   GtkTreeSelection     *selection)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+  GtkTreeModel *model = NULL;
+  GtkTreeIter iter;
+  GtkListStore *callers_store;
+  g_autoptr(GHashTable) callers = NULL;
+  g_autoptr(GHashTable) processed = NULL;
+  StackNode *callees = NULL;
+  StackNode *node;
+
+  g_assert (SYSPROF_IS_CALLGRAPH_PAGE (self));
+  g_assert (GTK_IS_TREE_SELECTION (selection));
+
+  if (!gtk_tree_selection_get_selected (selection, &model, &iter))
+    {
+      gtk_tree_view_set_model (priv->callers_view, NULL);
+      gtk_tree_view_set_model (priv->descendants_view, NULL);
+      return;
+    }
+
+  gtk_tree_model_get (model, &iter,
+                      COLUMN_POINTER, &callees,
+                      -1);
+
+  sysprof_callgraph_page_update_descendants (self, callees);
+
+  callers_store = gtk_list_store_new (4,
+                                      G_TYPE_STRING,
+                                      G_TYPE_DOUBLE,
+                                      G_TYPE_DOUBLE,
+                                      G_TYPE_POINTER);
+
+  callers = g_hash_table_new_full (NULL, NULL, NULL, caller_free);
+  processed = g_hash_table_new (NULL, NULL);
+
+  for (node = callees; node != NULL; node = node->next)
+    {
+      Caller *c;
+
+      if (!node->parent)
+        continue;
+
+      c = g_hash_table_lookup (callers, U64_TO_POINTER (node->parent->data));
+
+      if (c == NULL)
+        {
+          c = caller_new (node->parent);
+          g_hash_table_insert (callers, (gpointer)c->name, c);
+        }
+    }
+
+  for (node = callees; node != NULL; node = node->next)
+    {
+      StackNode *top_caller = node->parent;
+      StackNode *top_callee = node;
+      StackNode *n;
+      Caller *c;
+
+      if (!node->parent)
+        continue;
+
+      /*
+       * We could have a situation where the function was called in a
+       * reentrant fashion, so we want to take the top-most match in the
+       * stack.
+       */
+      for (n = node; n && n->parent; n = n->parent)
+        {
+          if (n->data == node->data && n->parent->data == node->parent->data)
+            {
+              top_caller = n->parent;
+              top_callee = n;
+            }
+        }
+
+      c = g_hash_table_lookup (callers, U64_TO_POINTER (node->parent->data));
+
+      g_assert (c != NULL);
+
+      if (!g_hash_table_lookup (processed, top_caller))
+        {
+          c->total += top_callee->total;
+          g_hash_table_insert (processed, top_caller, top_caller);
+        }
+
+      c->self += node->size;
+    }
+
+  {
+    GHashTableIter hiter;
+    gpointer key, value;
+    guint size = 0;
+
+    size = MAX (1, sysprof_callgraph_page_get_profile_size (self));
+
+    g_hash_table_iter_init (&hiter, callers);
+
+    while (g_hash_table_iter_next (&hiter, &key, &value))
+      {
+        Caller *c = value;
+
+        gtk_list_store_append (callers_store, &iter);
+        gtk_list_store_set (callers_store, &iter,
+                            COLUMN_NAME, c->name,
+                            COLUMN_SELF, c->self * 100.0 / size,
+                            COLUMN_TOTAL, c->total * 100.0 / size,
+                            COLUMN_POINTER, c->node,
+                            -1);
+      }
+  }
+
+  gtk_tree_view_set_model (priv->callers_view, GTK_TREE_MODEL (callers_store));
+  gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (callers_store),
+                                        COLUMN_TOTAL,
+                                        GTK_SORT_DESCENDING);
+
+  g_clear_object (&callers_store);
+}
+
+static void
+sysprof_callgraph_page_set_node (SysprofCallgraphPage *self,
+                                 StackNode            *node)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (SYSPROF_IS_CALLGRAPH_PAGE (self));
+  g_assert (node != NULL);
+
+  if (priv->profile == NULL)
+    return;
+
+  model = gtk_tree_view_get_model (priv->functions_view);
+
+  if (gtk_tree_model_get_iter_first (model, &iter))
+    {
+      do
+        {
+          StackNode *item = NULL;
+
+          gtk_tree_model_get (model, &iter,
+                              COLUMN_POINTER, &item,
+                              -1);
+
+          if (item != NULL && item->data == node->data)
+            {
+              GtkTreeSelection *selection;
+
+              selection = gtk_tree_view_get_selection (priv->functions_view);
+              gtk_tree_selection_select_iter (selection, &iter);
+
+              break;
+            }
+        }
+      while (gtk_tree_model_iter_next (model, &iter));
+    }
+}
+
+static void
+sysprof_callgraph_page_descendant_activated (SysprofCallgraphPage *self,
+                                             GtkTreePath          *path,
+                                             GtkTreeViewColumn    *column,
+                                             GtkTreeView          *tree_view)
+{
+  GtkTreeModel *model;
+  StackNode *node = NULL;
+  GtkTreeIter iter;
+
+  g_assert (SYSPROF_IS_CALLGRAPH_PAGE (self));
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+  g_assert (path != NULL);
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+
+  model = gtk_tree_view_get_model (tree_view);
+
+  if (!gtk_tree_model_get_iter (model, &iter, path))
+    return;
+
+  gtk_tree_model_get (model, &iter,
+                      COLUMN_POINTER, &node,
+                      -1);
+
+  if (node != NULL)
+    sysprof_callgraph_page_set_node (self, node);
+}
+
+static void
+sysprof_callgraph_page_caller_activated (SysprofCallgraphPage   *self,
+                                    GtkTreePath       *path,
+                                    GtkTreeViewColumn *column,
+                                    GtkTreeView       *tree_view)
+{
+  GtkTreeModel *model;
+  StackNode *node = NULL;
+  GtkTreeIter iter;
+
+  g_assert (SYSPROF_IS_CALLGRAPH_PAGE (self));
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+  g_assert (path != NULL);
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+
+  model = gtk_tree_view_get_model (tree_view);
+
+  if (!gtk_tree_model_get_iter (model, &iter, path))
+    return;
+
+  gtk_tree_model_get (model, &iter,
+                      COLUMN_POINTER, &node,
+                      -1);
+
+  if (node != NULL)
+    sysprof_callgraph_page_set_node (self, node);
+}
+
+static void
+sysprof_callgraph_page_tag_data_func (GtkTreeViewColumn *column,
+                                      GtkCellRenderer   *cell,
+                                      GtkTreeModel      *model,
+                                      GtkTreeIter       *iter,
+                                      gpointer           data)
+{
+  SysprofCallgraphPage *self = data;
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+  StackNode *node = NULL;
+  const gchar *str = NULL;
+
+  if (priv->profile == NULL)
+    return;
+
+  gtk_tree_model_get (model, iter, COLUMN_POINTER, &node, -1);
+
+  if (node && node->data)
+    {
+      GQuark tag;
+
+      tag = sysprof_callgraph_profile_get_tag (priv->profile, GSIZE_TO_POINTER (node->data));
+      if (tag != 0)
+        str = g_quark_to_string (tag);
+    }
+
+  g_object_set (cell, "text", str, NULL);
+}
+
+static void
+sysprof_callgraph_page_real_go_previous (SysprofCallgraphPage *self)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+  StackNode *node;
+
+  g_assert (SYSPROF_IS_CALLGRAPH_PAGE (self));
+
+  node = g_queue_pop_head (priv->history);
+
+  if (NULL != (node = g_queue_peek_head (priv->history)))
+    sysprof_callgraph_page_set_node (self, node);
+}
+
+static void
+descendants_view_move_cursor_cb (GtkTreeView     *descendants_view,
+                                 GtkMovementStep  step,
+                                 int              direction,
+                                 gpointer         user_data)
+{
+  if (step == GTK_MOVEMENT_VISUAL_POSITIONS)
+    {
+      GtkTreePath *path;
+
+      gtk_tree_view_get_cursor (descendants_view, &path, NULL);
+
+      if (direction == 1)
+        {
+          gtk_tree_view_expand_row (descendants_view, path, FALSE);
+          g_signal_stop_emission_by_name (descendants_view, "move-cursor");
+        }
+      else if (direction == -1)
+        {
+          gtk_tree_view_collapse_row (descendants_view, path);
+          g_signal_stop_emission_by_name (descendants_view, "move-cursor");
+        }
+
+      gtk_tree_path_free (path);
+    }
+}
+
+static void
+copy_tree_view_selection_cb (GtkTreeModel *model,
+                             GtkTreePath  *path,
+                             GtkTreeIter  *iter,
+                             gpointer      data)
+{
+  g_autofree gchar *name = NULL;
+  gchar sstr[16];
+  gchar tstr[16];
+  GString *str = data;
+  gdouble self;
+  gdouble total;
+  gint depth;
+
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (path != NULL);
+  g_assert (iter != NULL);
+  g_assert (str != NULL);
+
+  depth = gtk_tree_path_get_depth (path);
+  gtk_tree_model_get (model, iter,
+                      COLUMN_NAME, &name,
+                      COLUMN_SELF, &self,
+                      COLUMN_TOTAL, &total,
+                      -1);
+
+  g_snprintf (sstr, sizeof sstr, "%.2lf%%", self);
+  g_snprintf (tstr, sizeof tstr, "%.2lf%%", total);
+
+  g_string_append_printf (str, "[%8s] [%8s]    ", sstr, tstr);
+
+  for (gint i = 1; i < depth; i++)
+    g_string_append (str, "  ");
+  g_string_append (str, name);
+  g_string_append_c (str, '\n');
+}
+
+static void
+copy_tree_view_selection (GtkTreeView *tree_view)
+{
+  g_autoptr(GString) str = NULL;
+  GtkClipboard *clipboard;
+
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+
+  str = g_string_new ("      SELF      TOTAL    FUNCTION\n");
+  gtk_tree_selection_selected_foreach (gtk_tree_view_get_selection (tree_view),
+                                       copy_tree_view_selection_cb,
+                                       str);
+
+  clipboard = gtk_widget_get_clipboard (GTK_WIDGET (tree_view), GDK_SELECTION_CLIPBOARD);
+  gtk_clipboard_set_text (clipboard, str->str, str->len);
+}
+
+static void
+sysprof_callgraph_page_copy_cb (GtkWidget            *widget,
+                                SysprofCallgraphPage *self)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+  GtkWidget *toplevel;
+  GtkWidget *focus;
+
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (SYSPROF_IS_CALLGRAPH_PAGE (self));
+
+  if (!(toplevel = gtk_widget_get_toplevel (widget)) ||
+      !GTK_IS_WINDOW (toplevel) ||
+      !(focus = gtk_window_get_focus (GTK_WINDOW (toplevel))))
+    return;
+
+  if (focus == GTK_WIDGET (priv->descendants_view))
+    copy_tree_view_selection (priv->descendants_view);
+  else if (focus == GTK_WIDGET (priv->callers_view))
+    copy_tree_view_selection (priv->callers_view);
+  else if (focus == GTK_WIDGET (priv->functions_view))
+    copy_tree_view_selection (priv->functions_view);
+}
+
+static void
+sysprof_callgraph_page_generate_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  SysprofProfile *profile = (SysprofProfile *)object;
+  SysprofCallgraphPage *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (SYSPROF_IS_PROFILE (profile));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+
+  if (!sysprof_profile_generate_finish (profile, result, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    sysprof_callgraph_page_set_profile (self, SYSPROF_CALLGRAPH_PROFILE (profile));
+}
+
+static void
+sysprof_callgraph_page_load_async (SysprofPage             *page,
+                                   SysprofCaptureReader    *reader,
+                                   SysprofSelection        *selection,
+                                   SysprofCaptureCondition *filter,
+                                   GCancellable            *cancellable,
+                                   GAsyncReadyCallback      callback,
+                                   gpointer                 user_data)
+{
+  SysprofCallgraphPage *self = (SysprofCallgraphPage *)page;
+  g_autoptr(SysprofCaptureReader) copy = NULL;
+  g_autoptr(SysprofProfile) profile = NULL;
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (SYSPROF_IS_CALLGRAPH_PAGE (self));
+  g_assert (reader != NULL);
+  g_assert (SYSPROF_IS_SELECTION (selection));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_callgraph_page_load_async);
+
+  copy = sysprof_capture_reader_copy (reader);
+
+  profile = sysprof_callgraph_profile_new_with_selection (selection);
+  sysprof_profile_set_reader (profile, reader);
+  sysprof_profile_generate (profile,
+                            cancellable,
+                            sysprof_callgraph_page_generate_cb,
+                            g_steal_pointer (&task));
+}
+
+static gboolean
+sysprof_callgraph_page_load_finish (SysprofPage   *page,
+                                    GAsyncResult  *result,
+                                    GError       **error)
+{
+  g_return_val_if_fail (SYSPROF_IS_CALLGRAPH_PAGE (page), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+sysprof_callgraph_page_finalize (GObject *object)
+{
+  SysprofCallgraphPage *self = (SysprofCallgraphPage *)object;
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+
+  g_clear_pointer (&priv->history, g_queue_free);
+  g_clear_object (&priv->profile);
+
+  G_OBJECT_CLASS (sysprof_callgraph_page_parent_class)->finalize (object);
+}
+
+static void
+sysprof_callgraph_page_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  SysprofCallgraphPage *self = SYSPROF_CALLGRAPH_PAGE (object);
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_PROFILE:
+      g_value_set_object (value, priv->profile);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+sysprof_callgraph_page_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  SysprofCallgraphPage *self = SYSPROF_CALLGRAPH_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROFILE:
+      sysprof_callgraph_page_set_profile (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+sysprof_callgraph_page_class_init (SysprofCallgraphPageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  SysprofPageClass *page_class = SYSPROF_PAGE_CLASS (klass);
+  GtkBindingSet *bindings;
+
+  object_class->finalize = sysprof_callgraph_page_finalize;
+  object_class->get_property = sysprof_callgraph_page_get_property;
+  object_class->set_property = sysprof_callgraph_page_set_property;
+
+  page_class->load_async = sysprof_callgraph_page_load_async;
+  page_class->load_finish = sysprof_callgraph_page_load_finish;
+
+  klass->go_previous = sysprof_callgraph_page_real_go_previous;
+
+  properties [PROP_PROFILE] =
+    g_param_spec_object ("profile",
+                         "Profile",
+                         "The callgraph profile to view",
+                         SYSPROF_TYPE_CALLGRAPH_PROFILE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [GO_PREVIOUS] =
+    g_signal_new ("go-previous",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (SysprofCallgraphPageClass, go_previous),
+                  NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/org/gnome/sysprof/ui/sysprof-callgraph-page.ui");
+
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofCallgraphPage, callers_view);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofCallgraphPage, functions_view);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofCallgraphPage, descendants_view);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofCallgraphPage, descendants_name_column);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofCallgraphPage, stack);
+
+  bindings = gtk_binding_set_by_class (klass);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Left, GDK_MOD1_MASK, "go-previous", 0);
+
+  g_type_ensure (SYSPROF_TYPE_CELL_RENDERER_PERCENT);
+}
+
+static void
+sysprof_callgraph_page_init (SysprofCallgraphPage *self)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+  DzlShortcutController *controller;
+  GtkTreeSelection *selection;
+  GtkCellRenderer *cell;
+
+  priv->history = g_queue_new ();
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_stack_set_visible_child_name (priv->stack, "empty-state");
+
+  selection = gtk_tree_view_get_selection (priv->functions_view);
+
+  g_signal_connect_object (selection,
+                           "changed",
+                           G_CALLBACK (sysprof_callgraph_page_function_selection_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->descendants_view,
+                           "row-activated",
+                           G_CALLBACK (sysprof_callgraph_page_descendant_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->callers_view,
+                           "row-activated",
+                           G_CALLBACK (sysprof_callgraph_page_caller_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect (priv->descendants_view,
+                    "move-cursor",
+                    G_CALLBACK (descendants_view_move_cursor_cb),
+                    NULL);
+
+  cell = g_object_new (GTK_TYPE_CELL_RENDERER_TEXT,
+                       "ellipsize", PANGO_ELLIPSIZE_MIDDLE,
+                       "xalign", 0.0f,
+                       NULL);
+  gtk_tree_view_column_pack_start (priv->descendants_name_column, cell, TRUE);
+  gtk_tree_view_column_add_attribute (priv->descendants_name_column, cell, "text", 0);
+
+  cell = g_object_new (GTK_TYPE_CELL_RENDERER_TEXT,
+                       "foreground", "#666666",
+                       "scale", PANGO_SCALE_SMALL,
+                       "xalign", 1.0f,
+                       NULL);
+  gtk_tree_view_column_pack_start (priv->descendants_name_column, cell, FALSE);
+  gtk_tree_view_column_set_cell_data_func (priv->descendants_name_column, cell,
+                                           sysprof_callgraph_page_tag_data_func,
+                                           self, NULL);
+
+  gtk_tree_selection_set_mode (gtk_tree_view_get_selection (priv->descendants_view),
+                               GTK_SELECTION_MULTIPLE);
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+
+  dzl_shortcut_controller_add_command_callback (controller,
+                                                "org.gnome.sysprof3.capture.copy",
+                                                "<Control>c",
+                                                DZL_SHORTCUT_PHASE_BUBBLE,
+                                                (GtkCallback) sysprof_callgraph_page_copy_cb,
+                                                self,
+                                                NULL);
+}
+
+typedef struct _Descendant Descendant;
+
+struct _Descendant
+{
+  const gchar *name;
+  guint        self;
+  guint        cumulative;
+  Descendant  *parent;
+  Descendant  *siblings;
+  Descendant  *children;
+};
+
+static void
+build_tree_cb (StackLink *trace,
+               gint       size,
+               gpointer   user_data)
+{
+  Descendant **tree = user_data;
+  Descendant *parent = NULL;
+  StackLink *link;
+
+  g_assert (trace != NULL);
+  g_assert (tree != NULL);
+
+  /* Get last item */
+  link = trace;
+  while (link->next)
+    link = link->next;
+
+  for (; link != NULL; link = link->prev)
+    {
+      const gchar *address = U64_TO_POINTER (link->data);
+      Descendant *prev = NULL;
+      Descendant *match = NULL;
+
+      for (match = *tree; match != NULL; match = match->siblings)
+        {
+          if (match->name == address)
+            {
+              if (prev != NULL)
+                {
+                  /* Move to front */
+                  prev->siblings = match->siblings;
+                  match->siblings = *tree;
+                  *tree = match;
+                }
+              break;
+            }
+        }
+
+      if (match == NULL)
+        {
+          /* Have we seen this object further up the tree? */
+          for (match = parent; match != NULL; match = match->parent)
+            {
+              if (match->name == address)
+                break;
+            }
+        }
+
+      if (match == NULL)
+        {
+          match = g_slice_new (Descendant);
+          match->name = address;
+          match->cumulative = 0;
+          match->self = 0;
+          match->children = NULL;
+          match->parent = parent;
+          match->siblings = *tree;
+          *tree = match;
+        }
+
+      tree = &match->children;
+      parent = match;
+    }
+
+  parent->self += size;
+
+  for (; parent != NULL; parent = parent->parent)
+    parent->cumulative += size;
+}
+
+static Descendant *
+build_tree (StackNode *node)
+{
+  Descendant *tree = NULL;
+
+  for (; node != NULL; node = node->next)
+    {
+      if (node->toplevel)
+        stack_node_foreach_trace (node, build_tree_cb, &tree);
+    }
+
+  return tree;
+}
+
+static void
+append_to_tree_and_free (SysprofCallgraphPage *self,
+                         StackStash           *stash,
+                         GtkTreeStore         *store,
+                         Descendant           *item,
+                         GtkTreeIter          *parent)
+{
+  StackNode *node = NULL;
+  GtkTreeIter iter;
+  guint profile_size;
+
+  g_assert (GTK_IS_TREE_STORE (store));
+  g_assert (item != NULL);
+
+  profile_size = MAX (1, sysprof_callgraph_page_get_profile_size (self));
+
+  gtk_tree_store_append (store, &iter, parent);
+
+  node = stack_stash_find_node (stash, (gpointer)item->name);
+
+  gtk_tree_store_set (store, &iter,
+                      COLUMN_NAME, item->name,
+                      COLUMN_SELF, item->self * 100.0 / (gdouble)profile_size,
+                      COLUMN_TOTAL, item->cumulative * 100.0 / (gdouble)profile_size,
+                      COLUMN_POINTER, node,
+                      COLUMN_HITS, (guint)item->cumulative,
+                      -1);
+
+  if (item->siblings != NULL)
+    append_to_tree_and_free (self, stash, store, item->siblings, parent);
+
+  if (item->children != NULL)
+    append_to_tree_and_free (self, stash, store, item->children, &iter);
+
+  g_slice_free (Descendant, item);
+}
+
+static void
+sysprof_callgraph_page_update_descendants (SysprofCallgraphPage *self,
+                                           StackNode            *node)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+  GtkTreeStore *store;
+
+  g_assert (SYSPROF_IS_CALLGRAPH_PAGE (self));
+
+  if (g_queue_peek_head (priv->history) != node)
+    g_queue_push_head (priv->history, node);
+
+  store = gtk_tree_store_new (5,
+                              G_TYPE_STRING,
+                              G_TYPE_DOUBLE,
+                              G_TYPE_DOUBLE,
+                              G_TYPE_POINTER,
+                              G_TYPE_UINT);
+
+  if (priv->profile != NULL)
+  {
+    StackStash *stash;
+
+    stash = sysprof_callgraph_profile_get_stash (priv->profile);
+    if (stash != NULL)
+      {
+        Descendant *tree;
+
+        tree = build_tree (node);
+        if (tree != NULL)
+          append_to_tree_and_free (self, stash, store, tree, NULL);
+      }
+  }
+
+  gtk_tree_view_set_model (priv->descendants_view, GTK_TREE_MODEL (store));
+  gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (store),
+                                        COLUMN_TOTAL, GTK_SORT_DESCENDING);
+  sysprof_callgraph_page_expand_descendants (self);
+
+  g_clear_object (&store);
+}
+
+/**
+ * sysprof_callgraph_page_screenshot:
+ * @self: A #SysprofCallgraphPage.
+ *
+ * This function will generate a text representation of the descendants tree.
+ * This is useful if you want to include various profiling information in a
+ * commit message or email.
+ *
+ * The text generated will match the current row expansion in the tree view.
+ *
+ * Returns: (nullable) (transfer full): A newly allocated string that should be freed
+ *   with g_free().
+ */
+gchar *
+sysprof_callgraph_page_screenshot (SysprofCallgraphPage *self)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+  GtkTreeView *tree_view;
+  GtkTreeModel *model;
+  GtkTreePath *tree_path;
+  GString *str;
+  GtkTreeIter iter;
+
+  g_return_val_if_fail (SYSPROF_IS_CALLGRAPH_PAGE (self), NULL);
+
+  tree_view = priv->descendants_view;
+
+  if (NULL == (model = gtk_tree_view_get_model (tree_view)))
+    return NULL;
+
+  /*
+   * To avoid having to precalculate the deepest visible row, we
+   * put the timing information at the beginning of the line.
+   */
+
+  str = g_string_new ("      SELF CUMULATIVE    FUNCTION\n");
+  tree_path = gtk_tree_path_new_first ();
+
+  for (;;)
+    {
+      if (gtk_tree_model_get_iter (model, &iter, tree_path))
+        {
+          guint depth = gtk_tree_path_get_depth (tree_path);
+          StackNode *node;
+          gdouble in_self;
+          gdouble total;
+          guint i;
+
+          gtk_tree_model_get (model, &iter,
+                              COLUMN_SELF, &in_self,
+                              COLUMN_TOTAL, &total,
+                              COLUMN_POINTER, &node,
+                              -1);
+
+          g_string_append_printf (str, "[% 7.2lf%%] [% 7.2lf%%]  ", in_self, total);
+
+          for (i = 0; i < depth; i++)
+            g_string_append (str, "  ");
+          g_string_append (str, GSIZE_TO_POINTER (node->data));
+          g_string_append_c (str, '\n');
+
+          if (gtk_tree_view_row_expanded (tree_view, tree_path))
+            gtk_tree_path_down (tree_path);
+          else
+            gtk_tree_path_next (tree_path);
+
+          continue;
+        }
+
+      if (!gtk_tree_path_up (tree_path) || !gtk_tree_path_get_depth (tree_path))
+        break;
+
+      gtk_tree_path_next (tree_path);
+    }
+
+  gtk_tree_path_free (tree_path);
+
+  return g_string_free (str, FALSE);
+}
+
+guint
+sysprof_callgraph_page_get_n_functions (SysprofCallgraphPage *self)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+  GtkTreeModel *model;
+  guint ret = 0;
+
+  g_return_val_if_fail (SYSPROF_IS_CALLGRAPH_PAGE (self), 0);
+
+  if (NULL != (model = gtk_tree_view_get_model (priv->functions_view)))
+    ret = gtk_tree_model_iter_n_children (model, NULL);
+
+  return ret;
+}
+
+void
+_sysprof_callgraph_page_set_loading (SysprofCallgraphPage *self,
+                                     gboolean              loading)
+{
+  SysprofCallgraphPagePrivate *priv = sysprof_callgraph_page_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_CALLGRAPH_PAGE (self));
+
+  if (loading)
+    priv->loading++;
+  else
+    priv->loading--;
+
+  if (priv->loading)
+    gtk_stack_set_visible_child_name (priv->stack, "loading");
+  else
+    gtk_stack_set_visible_child_name (priv->stack, "callgraph");
+}
diff --git a/src/libsysprof-ui/sysprof-callgraph-page.h b/src/libsysprof-ui/sysprof-callgraph-page.h
new file mode 100644
index 0000000..7624c58
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-callgraph-page.h
@@ -0,0 +1,51 @@
+/* sysprof-callgraph-page.h
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <sysprof.h>
+
+#include "sysprof-page.h"
+
+G_BEGIN_DECLS
+
+#define SYSPROF_TYPE_CALLGRAPH_PAGE (sysprof_callgraph_page_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (SysprofCallgraphPage, sysprof_callgraph_page, SYSPROF, CALLGRAPH_PAGE, SysprofPage)
+
+struct _SysprofCallgraphPageClass
+{
+  SysprofPageClass parent_class;
+
+  void (*go_previous) (SysprofCallgraphPage *self);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+GtkWidget               *sysprof_callgraph_page_new             (void);
+SysprofCallgraphProfile *sysprof_callgraph_page_get_profile     (SysprofCallgraphPage    *self);
+void                     sysprof_callgraph_page_set_profile     (SysprofCallgraphPage    *self,
+                                                                 SysprofCallgraphProfile *profile);
+gchar                   *sysprof_callgraph_page_screenshot      (SysprofCallgraphPage    *self);
+guint                    sysprof_callgraph_page_get_n_functions (SysprofCallgraphPage    *self);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-callgraph-page.ui b/src/libsysprof-ui/sysprof-callgraph-page.ui
new file mode 100644
index 0000000..2642e01
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-callgraph-page.ui
@@ -0,0 +1,235 @@
+<interface>
+  <template class="SysprofCallgraphPage" parent="SysprofPage">
+    <child>
+      <object class="GtkStack" id="stack">
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkPaned">
+            <property name="orientation">horizontal</property>
+            <property name="position">450</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkPaned">
+                <property name="orientation">vertical</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkScrolledWindow">
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkTreeView" id="functions_view">
+                        <property name="fixed-height-mode">true</property>
+                        <property name="visible">true</property>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="function_name_column">
+                            <property name="expand">true</property>
+                            <property name="sizing">fixed</property>
+                            <property name="sort-column-id">0</property>
+                            <property name="title" translatable="yes">Functions</property>
+                            <child>
+                              <object class="GtkCellRendererText">
+                                <property name="ellipsize">middle</property>
+                              </object>
+                              <attributes>
+                                <attribute name="text">0</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="function_self_column">
+                            <property name="expand">false</property>
+                            <property name="sizing">fixed</property>
+                            <property name="sort-column-id">1</property>
+                            <property name="title" translatable="yes">Self</property>
+                            <child>
+                              <object class="SysprofCellRendererPercent">
+                                <property name="width">65</property>
+                              </object>
+                              <attributes>
+                                <attribute name="percent">1</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="function_total_column">
+                            <property name="expand">false</property>
+                            <property name="sizing">fixed</property>
+                            <property name="sort-column-id">2</property>
+                            <property name="title" translatable="yes">Total</property>
+                            <child>
+                              <object class="SysprofCellRendererPercent">
+                                <property name="width">65</property>
+                              </object>
+                              <attributes>
+                                <attribute name="percent">2</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="resize">true</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkScrolledWindow">
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkTreeView" id="callers_view">
+                        <property name="visible">true</property>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="callers_name_column">
+                            <property name="expand">true</property>
+                            <property name="sizing">fixed</property>
+                            <property name="sort-column-id">0</property>
+                            <property name="title" translatable="yes">Callers</property>
+                            <child>
+                              <object class="GtkCellRendererText">
+                                <property name="ellipsize">middle</property>
+                              </object>
+                              <attributes>
+                                <attribute name="text">0</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="callers_self_column">
+                            <property name="expand">false</property>
+                            <property name="sizing">fixed</property>
+                            <property name="sort-column-id">1</property>
+                            <property name="title" translatable="yes">Self</property>
+                            <child>
+                              <object class="SysprofCellRendererPercent">
+                                <property name="width">65</property>
+                              </object>
+                              <attributes>
+                                <attribute name="percent">1</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="callers_total_column">
+                            <property name="expand">false</property>
+                            <property name="sizing">fixed</property>
+                            <property name="sort-column-id">2</property>
+                            <property name="title" translatable="yes">Total</property>
+                            <child>
+                              <object class="SysprofCellRendererPercent">
+                                <property name="width">65</property>
+                              </object>
+                              <attributes>
+                                <attribute name="percent">2</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="resize">true</property>
+                  </packing>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow">
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkTreeView" id="descendants_view">
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkTreeViewColumn" id="descendants_name_column">
+                        <property name="expand">true</property>
+                        <property name="sizing">autosize</property>
+                        <property name="sort-column-id">0</property>
+                        <property name="title" translatable="yes">Descendants</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn" id="descendants_self_column">
+                        <property name="expand">false</property>
+                        <property name="sizing">fixed</property>
+                        <property name="sort-column-id">1</property>
+                        <property name="title" translatable="yes">Self</property>
+                        <child>
+                          <object class="SysprofCellRendererPercent">
+                            <property name="width">65</property>
+                          </object>
+                          <attributes>
+                            <attribute name="percent">1</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn" id="descendants_total_column">
+                        <property name="expand">false</property>
+                        <property name="sizing">fixed</property>
+                        <property name="sort-column-id">2</property>
+                        <property name="title" translatable="yes">Total</property>
+                        <child>
+                          <object class="SysprofCellRendererPercent">
+                            <property name="width">65</property>
+                          </object>
+                          <attributes>
+                            <attribute name="percent">2</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn" id="function_hits_column">
+                        <property name="expand">false</property>
+                        <property name="sizing">fixed</property>
+                        <property name="title" translatable="yes">Hits</property>
+                        <child>
+                          <object class="GtkCellRendererText">
+                            <property name="xalign">1.0</property>
+                          </object>
+                          <attributes>
+                            <attribute name="text">4</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="name">callgraph</property>
+          </packing>
+        </child>
+        <child>
+          <object class="DzlEmptyState">
+            <property name="icon-name">content-loading-symbolic</property>
+            <property name="title" translatable="yes">Generating Callgraph</property>
+            <property name="subtitle" translatable="yes">Sysprof is busy creating the selected 
callgraph.</property>
+            <property name="visible">true</property>
+          </object>
+          <packing>
+            <property name="name">loading</property>
+          </packing>
+        </child>
+        <child>
+          <object class="DzlEmptyState">
+            <property name="icon-name">computer-fail-symbolic</property>
+            <property name="title" translatable="yes">Not Enough Samples</property>
+            <property name="subtitle" translatable="yes">More samples are necessary to display a 
callgraph.</property>
+            <property name="visible">false</property>
+          </object>
+          <packing>
+            <property name="name">empty-state</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libsysprof-ui/sysprof-callgraph-view.h b/src/libsysprof-ui/sysprof-callgraph-view.h
index 6cd68c5..7b0c2ec 100644
--- a/src/libsysprof-ui/sysprof-callgraph-view.h
+++ b/src/libsysprof-ui/sysprof-callgraph-view.h
@@ -20,10 +20,6 @@
 
 #pragma once
 
-#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
-# error "Only <sysprof-ui.h> can be included directly."
-#endif
-
 #include <gtk/gtk.h>
 #include <sysprof.h>
 
@@ -31,7 +27,6 @@ G_BEGIN_DECLS
 
 #define SYSPROF_TYPE_CALLGRAPH_VIEW (sysprof_callgraph_view_get_type())
 
-SYSPROF_AVAILABLE_IN_ALL
 G_DECLARE_DERIVABLE_TYPE (SysprofCallgraphView, sysprof_callgraph_view, SYSPROF, CALLGRAPH_VIEW, GtkBin)
 
 struct _SysprofCallgraphViewClass
@@ -39,20 +34,13 @@ struct _SysprofCallgraphViewClass
   GtkBinClass parent_class;
 
   void (*go_previous) (SysprofCallgraphView *self);
-
-  gpointer padding[8];
 };
 
-SYSPROF_AVAILABLE_IN_ALL
 GtkWidget               *sysprof_callgraph_view_new             (void);
-SYSPROF_AVAILABLE_IN_ALL
 SysprofCallgraphProfile *sysprof_callgraph_view_get_profile     (SysprofCallgraphView    *self);
-SYSPROF_AVAILABLE_IN_ALL
 void                     sysprof_callgraph_view_set_profile     (SysprofCallgraphView    *self,
                                                                  SysprofCallgraphProfile *profile);
-SYSPROF_AVAILABLE_IN_ALL
 gchar                   *sysprof_callgraph_view_screenshot      (SysprofCallgraphView    *self);
-SYSPROF_AVAILABLE_IN_ALL
 guint                    sysprof_callgraph_view_get_n_functions (SysprofCallgraphView    *self);
 
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-cell-renderer-duration.c 
b/src/libsysprof-ui/sysprof-cell-renderer-duration.c
index 49c4017..462b83e 100644
--- a/src/libsysprof-ui/sysprof-cell-renderer-duration.c
+++ b/src/libsysprof-ui/sysprof-cell-renderer-duration.c
@@ -110,7 +110,7 @@ sysprof_cell_renderer_duration_render (GtkCellRenderer      *renderer,
 
   if (r.width > 3)
     {
-      _sysprof_rounded_rectangle (cr, &r, 2, 2);
+      dzl_cairo_rounded_rectangle (cr, &r, 2, 2);
       cairo_fill (cr);
     }
   else if (r.width > 1)
diff --git a/src/libsysprof-ui/sysprof-cell-renderer-percent.c 
b/src/libsysprof-ui/sysprof-cell-renderer-percent.c
index 97aa0a0..d0d25d6 100644
--- a/src/libsysprof-ui/sysprof-cell-renderer-percent.c
+++ b/src/libsysprof-ui/sysprof-cell-renderer-percent.c
@@ -18,6 +18,8 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
+#define G_LOG_DOMAIN "sysprof-cell-renderer-percent"
+
 #include "config.h"
 
 #include <glib/gi18n.h>
diff --git a/src/libsysprof-ui/sysprof-cell-renderer-percent.h 
b/src/libsysprof-ui/sysprof-cell-renderer-percent.h
index 9c43720..b917ea7 100644
--- a/src/libsysprof-ui/sysprof-cell-renderer-percent.h
+++ b/src/libsysprof-ui/sysprof-cell-renderer-percent.h
@@ -20,14 +20,8 @@
 
 #pragma once
 
-#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
-# error "Only <sysprof-ui.h> can be included directly."
-#endif
-
 #include <gtk/gtk.h>
 
-#include "sysprof-version-macros.h"
-
 G_BEGIN_DECLS
 
 #define SYSPROF_TYPE_CELL_RENDERER_PERCENT            (sysprof_cell_renderer_percent_get_type())
@@ -50,16 +44,13 @@ struct _SysprofCellRendererPercentClass
 {
   GtkCellRendererProgressClass parent_class;
 
-  gpointer padding[4];
+  /*< private >*/
+  gpointer _reserved[4];
 };
 
-SYSPROF_AVAILABLE_IN_ALL
 GType            sysprof_cell_renderer_percent_get_type    (void);
-SYSPROF_AVAILABLE_IN_ALL
 GtkCellRenderer *sysprof_cell_renderer_percent_new         (void);
-SYSPROF_AVAILABLE_IN_ALL
 gdouble          sysprof_cell_renderer_percent_get_percent (SysprofCellRendererPercent *self);
-SYSPROF_AVAILABLE_IN_ALL
 void             sysprof_cell_renderer_percent_set_percent (SysprofCellRendererPercent *self,
                                                             gdouble                     percent);
 
diff --git a/src/libsysprof-ui/sysprof-color-cycle.c b/src/libsysprof-ui/sysprof-color-cycle.c
index d1c63c5..582878e 100644
--- a/src/libsysprof-ui/sysprof-color-cycle.c
+++ b/src/libsysprof-ui/sysprof-color-cycle.c
@@ -27,30 +27,47 @@
 G_DEFINE_BOXED_TYPE (SysprofColorCycle, sysprof_color_cycle, sysprof_color_cycle_ref, 
sysprof_color_cycle_unref)
 
 static const gchar *default_colors[] = {
-  "#73d216",
-  "#f57900",
-  "#3465a4",
-  "#ef2929",
-  "#75507b",
-  "#ce5c00",
-  "#c17d11",
-  "#cc0000",
-  "#edd400",
-  "#555753",
-  "#4e9a06",
-  "#204a87",
-  "#5c3566",
-  "#a40000",
-  "#c4a000",
-  "#8f5902",
-  "#2e3436",
-  "#8ae234",
-  "#729fcf",
-  "#ad7fa8",
-  "#fce94f",
-  "#fcaf3e",
-  "#e9b96e",
-  "#888a85",
+
+  "#1a5fb4", /* Blue 5 */
+  "#26a269", /* Green 5 */
+  "#e5a50a", /* Yellow 5 */
+  "#c64600", /* Orange 5 */
+  "#a51d2d", /* Red 5 */
+  "#613583", /* Purple 5 */
+  "#63452c", /* Brown 5 */
+
+  "#1c71d8", /* Blue 4 */
+  "#2ec27e", /* Green 4 */
+  "#f5c211", /* Yellow 4 */
+  "#e66100", /* Orange 4 */
+  "#c01c28", /* Red 4 */
+  "#813d9c", /* Purple 4 */
+  "#865e3c", /* Brown 4 */
+
+  "#3584e4", /* Blue 3 */
+  "#33d17a", /* Green 3 */
+  "#f6d32d", /* Yellow 3 */
+  "#ff7800", /* Orange 3 */
+  "#e01b24", /* Red 3 */
+  "#9141ac", /* Purple 3 */
+  "#986a44", /* Brown 3 */
+
+  "#62a0ea", /* Blue 2 */
+  "#57e389", /* Green 2 */
+  "#f8e45c", /* Yellow 2 */
+  "#ffa348", /* Orange 2 */
+  "#ed333b", /* Red 2 */
+  "#c061cb", /* Purple 2 */
+  "#b5835a", /* Brown 2 */
+
+  "#99c1f1", /* Blue 1 */
+  "#8ff0a4", /* Green 1 */
+  "#f9f06b", /* Yellow 1 */
+  "#ffbe6f", /* Orange 1 */
+  "#f66151", /* Red 1 */
+  "#dc8add", /* Purple 1 */
+  "#cdab8f", /* Brown 1 */
+
   NULL
 };
 
diff --git a/src/libsysprof-ui/sysprof-color-cycle.h b/src/libsysprof-ui/sysprof-color-cycle.h
index b512eed..650acb7 100644
--- a/src/libsysprof-ui/sysprof-color-cycle.h
+++ b/src/libsysprof-ui/sysprof-color-cycle.h
@@ -20,32 +20,22 @@
 
 #pragma once
 
-#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
-# error "Only <sysprof-ui.h> can be included directly."
-#endif
-
 #include <gtk/gtk.h>
 
-#include "sysprof-version-macros.h"
-
 G_BEGIN_DECLS
 
 #define SYSPROF_TYPE_COLOR_CYCLE (sysprof_color_cycle_get_type())
 
 typedef struct _SysprofColorCycle SysprofColorCycle;
 
-SYSPROF_AVAILABLE_IN_ALL
 GType              sysprof_color_cycle_get_type (void);
-SYSPROF_AVAILABLE_IN_ALL
 SysprofColorCycle *sysprof_color_cycle_ref      (SysprofColorCycle *self);
-SYSPROF_AVAILABLE_IN_ALL
 void               sysprof_color_cycle_unref    (SysprofColorCycle *self);
-SYSPROF_AVAILABLE_IN_ALL
 SysprofColorCycle *sysprof_color_cycle_new      (void);
-SYSPROF_AVAILABLE_IN_ALL
 void               sysprof_color_cycle_reset    (SysprofColorCycle *self);
-SYSPROF_AVAILABLE_IN_ALL
 void               sysprof_color_cycle_next     (SysprofColorCycle *self,
                                                  GdkRGBA           *rgba);
 
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SysprofColorCycle, sysprof_color_cycle_unref)
+
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-counters-aid.c b/src/libsysprof-ui/sysprof-counters-aid.c
new file mode 100644
index 0000000..86a9f27
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-counters-aid.c
@@ -0,0 +1,248 @@
+/* sysprof-counters-aid.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-counters-aid"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "sysprof-color-cycle.h"
+#include "sysprof-counters-aid.h"
+#include "sysprof-line-visualizer.h"
+#include "sysprof-marks-page.h"
+#include "sysprof-time-visualizer.h"
+
+struct _SysprofCountersAid
+{
+  SysprofAid parent_instance;
+};
+
+typedef struct
+{
+  SysprofCaptureCursor *cursor;
+  SysprofDisplay       *display;
+} Present;
+
+G_DEFINE_TYPE (SysprofCountersAid, sysprof_counters_aid, SYSPROF_TYPE_AID)
+
+static void
+present_free (gpointer data)
+{
+  Present *p = data;
+
+  g_clear_pointer (&p->cursor, sysprof_capture_cursor_unref);
+  g_clear_object (&p->display);
+  g_slice_free (Present, p);
+}
+
+static void
+on_group_activated_cb (SysprofVisualizerGroup *group,
+                       SysprofPage            *page)
+{
+  SysprofDisplay *display;
+
+  g_assert (SYSPROF_IS_VISUALIZER_GROUP (group));
+  g_assert (SYSPROF_IS_PAGE (page));
+
+  display = SYSPROF_DISPLAY (gtk_widget_get_ancestor (GTK_WIDGET (page), SYSPROF_TYPE_DISPLAY));
+  sysprof_display_set_visible_page (display, page);
+}
+
+/**
+ * sysprof_counters_aid_new:
+ *
+ * Create a new #SysprofCountersAid.
+ *
+ * Returns: (transfer full): a newly created #SysprofCountersAid
+ *
+ * Since: 3.34
+ */
+SysprofAid *
+sysprof_counters_aid_new (void)
+{
+  return g_object_new (SYSPROF_TYPE_COUNTERS_AID, NULL);
+}
+
+static void
+sysprof_counters_aid_prepare (SysprofAid      *self,
+                              SysprofProfiler *profiler)
+{
+}
+
+static gboolean
+collect_counters (const SysprofCaptureFrame *frame,
+                  gpointer                   user_data)
+{
+  SysprofCaptureCounterDefine *def = (SysprofCaptureCounterDefine *)frame;
+  GArray *counters = user_data;
+
+  g_assert (frame != NULL);
+  g_assert (frame->type == SYSPROF_CAPTURE_FRAME_CTRDEF);
+  g_assert (counters != NULL);
+
+  if (def->n_counters > 0)
+    g_array_append_vals (counters, def->counters, def->n_counters);
+
+  return TRUE;
+}
+
+static void
+sysprof_counters_aid_present_worker (GTask        *task,
+                                    gpointer      source_object,
+                                    gpointer      task_data,
+                                    GCancellable *cancellable)
+{
+  Present *present = task_data;
+  g_autoptr(GArray) counters = NULL;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (SYSPROF_IS_COUNTERS_AID (source_object));
+  g_assert (present != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  counters = g_array_new (FALSE, FALSE, sizeof (SysprofCaptureCounter));
+  sysprof_capture_cursor_foreach (present->cursor, collect_counters, counters);
+  g_task_return_pointer (task,
+                         g_steal_pointer (&counters),
+                         (GDestroyNotify) g_array_unref);
+}
+
+static void
+sysprof_counters_aid_present_async (SysprofAid           *aid,
+                                    SysprofCaptureReader *reader,
+                                    SysprofDisplay       *display,
+                                    GCancellable         *cancellable,
+                                    GAsyncReadyCallback   callback,
+                                    gpointer              user_data)
+{
+  static const SysprofCaptureFrameType types[] = { SYSPROF_CAPTURE_FRAME_CTRDEF };
+  g_autoptr(SysprofCaptureCondition) condition = NULL;
+  g_autoptr(SysprofCaptureCursor) cursor = NULL;
+  g_autoptr(GTask) task = NULL;
+  Present present;
+
+  g_assert (SYSPROF_IS_COUNTERS_AID (aid));
+  g_assert (reader != NULL);
+  g_assert (SYSPROF_IS_DISPLAY (display));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  condition = sysprof_capture_condition_new_where_type_in (1, types);
+  cursor = sysprof_capture_cursor_new (reader);
+  sysprof_capture_cursor_add_condition (cursor, g_steal_pointer (&condition));
+
+  present.cursor = g_steal_pointer (&cursor);
+  present.display = g_object_ref (display);
+
+  task = g_task_new (aid, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_counters_aid_present_async);
+  g_task_set_task_data (task,
+                        g_slice_dup (Present, &present),
+                        present_free);
+  g_task_run_in_thread (task, sysprof_counters_aid_present_worker);
+}
+
+static gboolean
+sysprof_counters_aid_present_finish (SysprofAid    *aid,
+                                     GAsyncResult  *result,
+                                     GError       **error)
+{
+  g_autoptr(GArray) counters = NULL;
+  Present *present;
+
+  g_assert (SYSPROF_IS_AID (aid));
+  g_assert (G_IS_TASK (result));
+
+  present = g_task_get_task_data (G_TASK (result));
+
+  if ((counters = g_task_propagate_pointer (G_TASK (result), error)) && counters->len > 0)
+    {
+      g_autoptr(SysprofColorCycle) cycle = sysprof_color_cycle_new ();
+      SysprofVisualizerGroup *group;
+      SysprofVisualizer *combined;
+      GtkWidget *page;
+
+      group = g_object_new (SYSPROF_TYPE_VISUALIZER_GROUP,
+                            "can-focus", TRUE,
+                            "has-page", TRUE,
+                            "title", _("Counters"),
+                            "visible", TRUE,
+                            NULL);
+
+      combined = g_object_new (SYSPROF_TYPE_TIME_VISUALIZER,
+                               "title", _("Counters"),
+                               "height-request", 35,
+                               "visible", TRUE,
+                               NULL);
+      sysprof_visualizer_group_insert (group, combined, -1, TRUE);
+
+      for (guint i = 0; i < counters->len; i++)
+        {
+          const SysprofCaptureCounter *ctr = &g_array_index (counters, SysprofCaptureCounter, i);
+          g_autofree gchar *title = g_strdup_printf ("%s — %s", ctr->category, ctr->name);
+          GtkWidget *row;
+          GdkRGBA rgba;
+
+          row = g_object_new (SYSPROF_TYPE_LINE_VISUALIZER,
+                              "title", title,
+                              "height-request", 35,
+                              "visible", FALSE,
+                              NULL);
+          sysprof_color_cycle_next (cycle, &rgba);
+          sysprof_line_visualizer_add_counter (SYSPROF_LINE_VISUALIZER (row), ctr->id, &rgba);
+          rgba.alpha = .5;
+          sysprof_line_visualizer_set_fill (SYSPROF_LINE_VISUALIZER (row), ctr->id, &rgba);
+          sysprof_time_visualizer_add_counter (SYSPROF_TIME_VISUALIZER (combined), ctr->id, &rgba);
+          sysprof_visualizer_group_insert (group, SYSPROF_VISUALIZER (row), -1, TRUE);
+        }
+
+      sysprof_display_add_group (present->display, group);
+
+      page = sysprof_marks_page_new (sysprof_display_get_zoom_manager (present->display),
+                                     SYSPROF_MARKS_MODEL_COUNTERS);
+      gtk_widget_show (page);
+
+      g_signal_connect_object (group,
+                               "group-activated",
+                               G_CALLBACK (on_group_activated_cb),
+                               page,
+                               0);
+      sysprof_display_add_page (present->display, SYSPROF_PAGE (page));
+    }
+
+  return counters != NULL;
+}
+
+static void
+sysprof_counters_aid_class_init (SysprofCountersAidClass *klass)
+{
+  SysprofAidClass *aid_class = SYSPROF_AID_CLASS (klass);
+
+  aid_class->prepare = sysprof_counters_aid_prepare;
+  aid_class->present_async = sysprof_counters_aid_present_async;
+  aid_class->present_finish = sysprof_counters_aid_present_finish;
+}
+
+static void
+sysprof_counters_aid_init (SysprofCountersAid *self)
+{
+  sysprof_aid_set_display_name (SYSPROF_AID (self), _("Battery"));
+  sysprof_aid_set_icon_name (SYSPROF_AID (self), "org.gnome.Sysprof3-symbolic");
+}
diff --git a/src/libsysprof-ui/sysprof-counters-aid.h b/src/libsysprof-ui/sysprof-counters-aid.h
new file mode 100644
index 0000000..5541b60
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-counters-aid.h
@@ -0,0 +1,33 @@
+/* sysprof-counters-aid.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "sysprof-aid.h"
+
+G_BEGIN_DECLS
+
+#define SYSPROF_TYPE_COUNTERS_AID (sysprof_counters_aid_get_type())
+
+G_DECLARE_FINAL_TYPE (SysprofCountersAid, sysprof_counters_aid, SYSPROF, COUNTERS_AID, SysprofAid)
+
+SysprofAid *sysprof_counters_aid_new (void);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-cpu-aid.c b/src/libsysprof-ui/sysprof-cpu-aid.c
index 22cd05e..31d913f 100644
--- a/src/libsysprof-ui/sysprof-cpu-aid.c
+++ b/src/libsysprof-ui/sysprof-cpu-aid.c
@@ -24,15 +24,33 @@
 
 #include <glib/gi18n.h>
 
+#include "sysprof-color-cycle.h"
 #include "sysprof-cpu-aid.h"
+#include "sysprof-line-visualizer.h"
 
 struct _SysprofCpuAid
 {
   SysprofAid parent_instance;
 };
 
+typedef struct
+{
+  SysprofCaptureCursor *cursor;
+  SysprofDisplay       *display;
+} Present;
+
 G_DEFINE_TYPE (SysprofCpuAid, sysprof_cpu_aid, SYSPROF_TYPE_AID)
 
+static void
+present_free (gpointer data)
+{
+  Present *p = data;
+
+  g_clear_pointer (&p->cursor, sysprof_capture_cursor_unref);
+  g_clear_object (&p->display);
+  g_slice_free (Present, p);
+}
+
 /**
  * sysprof_cpu_aid_new:
  *
@@ -63,12 +81,243 @@ sysprof_cpu_aid_prepare (SysprofAid      *self,
 #endif
 }
 
+static gboolean
+collect_cpu_counters (const SysprofCaptureFrame *frame,
+                      gpointer                   user_data)
+{
+  SysprofCaptureCounterDefine *def = (SysprofCaptureCounterDefine *)frame;
+  GArray *counters = user_data;
+
+  g_assert (frame != NULL);
+  g_assert (frame->type == SYSPROF_CAPTURE_FRAME_CTRDEF);
+  g_assert (counters != NULL);
+
+  for (guint i = 0; i < def->n_counters; i++)
+    {
+      const SysprofCaptureCounter *counter = &def->counters[i];
+
+      if (g_strcmp0 (counter->category, "CPU Percent") == 0 ||
+          g_strcmp0 (counter->category, "CPU Frequency") == 0)
+        g_array_append_vals (counters, counter, 1);
+    }
+
+  return TRUE;
+}
+
+static void
+sysprof_cpu_aid_present_worker (GTask        *task,
+                                gpointer      source_object,
+                                gpointer      task_data,
+                                GCancellable *cancellable)
+{
+  Present *present = task_data;
+  g_autoptr(GArray) counters = NULL;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (SYSPROF_IS_CPU_AID (source_object));
+  g_assert (present != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  counters = g_array_new (FALSE, FALSE, sizeof (SysprofCaptureCounter));
+  sysprof_capture_cursor_foreach (present->cursor, collect_cpu_counters, counters);
+  g_task_return_pointer (task,
+                         g_steal_pointer (&counters),
+                         (GDestroyNotify) g_array_unref);
+}
+
+static void
+sysprof_cpu_aid_present_async (SysprofAid           *aid,
+                               SysprofCaptureReader *reader,
+                               SysprofDisplay       *display,
+                               GCancellable         *cancellable,
+                               GAsyncReadyCallback   callback,
+                               gpointer              user_data)
+{
+  static const SysprofCaptureFrameType types[] = { SYSPROF_CAPTURE_FRAME_CTRDEF };
+  g_autoptr(SysprofCaptureCondition) condition = NULL;
+  g_autoptr(SysprofCaptureCursor) cursor = NULL;
+  g_autoptr(GTask) task = NULL;
+  Present present;
+
+  g_assert (SYSPROF_IS_CPU_AID (aid));
+  g_assert (reader != NULL);
+  g_assert (SYSPROF_IS_DISPLAY (display));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  condition = sysprof_capture_condition_new_where_type_in (1, types);
+  cursor = sysprof_capture_cursor_new (reader);
+  sysprof_capture_cursor_add_condition (cursor, g_steal_pointer (&condition));
+
+  present.cursor = g_steal_pointer (&cursor);
+  present.display = g_object_ref (display);
+
+  task = g_task_new (aid, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_cpu_aid_present_async);
+  g_task_set_task_data (task,
+                        g_slice_dup (Present, &present),
+                        present_free);
+  g_task_run_in_thread (task, sysprof_cpu_aid_present_worker);
+}
+
+static gboolean
+sysprof_cpu_aid_present_finish (SysprofAid    *aid,
+                                GAsyncResult  *result,
+                                GError       **error)
+{
+  g_autoptr(GArray) counters = NULL;
+  Present *present;
+
+  g_assert (SYSPROF_IS_AID (aid));
+  g_assert (G_IS_TASK (result));
+
+  present = g_task_get_task_data (G_TASK (result));
+
+  if ((counters = g_task_propagate_pointer (G_TASK (result), error)))
+    {
+      g_autoptr(SysprofColorCycle) cycle = sysprof_color_cycle_new ();
+      g_autoptr(SysprofColorCycle) freq_cycle = sysprof_color_cycle_new ();
+      SysprofVisualizerGroup *usage;
+      SysprofVisualizerGroup *freq;
+      SysprofVisualizer *freq_row = NULL;
+      SysprofVisualizer *over_row = NULL;
+      gboolean found_combined = FALSE;
+      gboolean has_usage = FALSE;
+      gboolean has_freq = FALSE;
+
+      usage = g_object_new (SYSPROF_TYPE_VISUALIZER_GROUP,
+                            "can-focus", TRUE,
+                            "priority", -1000,
+                            "title", _("CPU Usage"),
+                            "visible", TRUE,
+                            NULL);
+
+      freq = g_object_new (SYSPROF_TYPE_VISUALIZER_GROUP,
+                           "can-focus", TRUE,
+                            "priority", -999,
+                           "title", _("CPU Frequency"),
+                           "visible", TRUE,
+                           NULL);
+      freq_row = g_object_new (SYSPROF_TYPE_LINE_VISUALIZER,
+                               "title", _("CPU Frequency (All)"),
+                               "height-request", 35,
+                               "visible", TRUE,
+                               "y-lower", 0.0,
+                               "y-upper", 100.0,
+                               NULL);
+      gtk_container_add (GTK_CONTAINER (freq), GTK_WIDGET (freq_row));
+
+      over_row = g_object_new (SYSPROF_TYPE_LINE_VISUALIZER,
+                               "title", _("CPU Usage (All)"),
+                               "height-request", 35,
+                               "visible", TRUE,
+                               "y-lower", 0.0,
+                               "y-upper", 100.0,
+                               NULL);
+
+      for (guint i = 0; i < counters->len; i++)
+        {
+          const SysprofCaptureCounter *ctr = &g_array_index (counters, SysprofCaptureCounter, i);
+
+          if (g_strcmp0 (ctr->category, "CPU Percent") == 0)
+            {
+              if (strstr (ctr->name, "Combined") != NULL)
+                {
+                  GtkWidget *row;
+                  GdkRGBA rgba;
+
+                  found_combined = TRUE;
+
+                  gdk_rgba_parse (&rgba, "#1a5fb4");
+                  row = g_object_new (SYSPROF_TYPE_LINE_VISUALIZER,
+                                      /* Translators: CPU is the processor. */
+                                      "title", _("CPU Usage (All)"),
+                                      "height-request", 35,
+                                      "visible", TRUE,
+                                      "y-lower", 0.0,
+                                      "y-upper", 100.0,
+                                      NULL);
+                  sysprof_line_visualizer_add_counter (SYSPROF_LINE_VISUALIZER (row), ctr->id, &rgba);
+                  rgba.alpha = 0.5;
+                  sysprof_line_visualizer_set_fill (SYSPROF_LINE_VISUALIZER (row), ctr->id, &rgba);
+                  sysprof_visualizer_group_insert (usage, SYSPROF_VISUALIZER (row), 0, FALSE);
+                  has_usage = TRUE;
+                }
+              else if (g_str_has_prefix (ctr->name, "Total CPU "))
+                {
+                  GtkWidget *row;
+                  GdkRGBA rgba;
+
+                  sysprof_color_cycle_next (cycle, &rgba);
+                  row = g_object_new (SYSPROF_TYPE_LINE_VISUALIZER,
+                                      "title", ctr->name,
+                                      "height-request", 35,
+                                      "visible", FALSE,
+                                      "y-lower", 0.0,
+                                      "y-upper", 100.0,
+                                      NULL);
+                  sysprof_line_visualizer_add_counter (SYSPROF_LINE_VISUALIZER (row), ctr->id, &rgba);
+                  sysprof_line_visualizer_add_counter (SYSPROF_LINE_VISUALIZER (over_row), ctr->id, &rgba);
+                  rgba.alpha = 0.5;
+                  sysprof_line_visualizer_set_fill (SYSPROF_LINE_VISUALIZER (row), ctr->id, &rgba);
+                  sysprof_visualizer_group_insert (usage, SYSPROF_VISUALIZER (row), -1, TRUE);
+                  has_usage = TRUE;
+                }
+            }
+          else if (g_strcmp0 (ctr->category, "CPU Frequency") == 0)
+            {
+              if (g_str_has_prefix (ctr->name, "CPU "))
+                {
+                  g_autofree gchar *title = g_strdup_printf ("%s Frequency", ctr->name);
+                  GtkWidget *row;
+                  GdkRGBA rgba;
+
+                  sysprof_color_cycle_next (freq_cycle, &rgba);
+                  sysprof_line_visualizer_add_counter (SYSPROF_LINE_VISUALIZER (freq_row), ctr->id, &rgba);
+                  sysprof_line_visualizer_set_dash (SYSPROF_LINE_VISUALIZER (freq_row), ctr->id, TRUE);
+
+                  row = g_object_new (SYSPROF_TYPE_LINE_VISUALIZER,
+                                      "title", title,
+                                      "height-request", 35,
+                                      "visible", FALSE,
+                                      "y-lower", 0.0,
+                                      "y-upper", 100.0,
+                                      NULL);
+                  sysprof_line_visualizer_add_counter (SYSPROF_LINE_VISUALIZER (row), ctr->id, &rgba);
+                  sysprof_line_visualizer_set_dash (SYSPROF_LINE_VISUALIZER (row), ctr->id, TRUE);
+                  sysprof_visualizer_group_insert (freq, SYSPROF_VISUALIZER (row), -1, TRUE);
+
+                  has_freq = TRUE;
+                }
+            }
+        }
+
+      if (has_usage && !found_combined)
+        sysprof_visualizer_group_insert (usage, over_row, 0, FALSE);
+      else
+        gtk_widget_destroy (GTK_WIDGET (over_row));
+
+      if (has_usage)
+        sysprof_display_add_group (present->display, usage);
+      else
+        gtk_widget_destroy (GTK_WIDGET (usage));
+
+      if (has_freq)
+        sysprof_display_add_group (present->display, freq);
+      else
+        gtk_widget_destroy (GTK_WIDGET (freq));
+    }
+
+  return counters != NULL;
+}
+
 static void
 sysprof_cpu_aid_class_init (SysprofCpuAidClass *klass)
 {
   SysprofAidClass *aid_class = SYSPROF_AID_CLASS (klass);
 
   aid_class->prepare = sysprof_cpu_aid_prepare;
+  aid_class->present_async = sysprof_cpu_aid_present_async;
+  aid_class->present_finish = sysprof_cpu_aid_present_finish;
 }
 
 static void
diff --git a/src/libsysprof-ui/sysprof-cpu-aid.h b/src/libsysprof-ui/sysprof-cpu-aid.h
index ca5899b..4769885 100644
--- a/src/libsysprof-ui/sysprof-cpu-aid.h
+++ b/src/libsysprof-ui/sysprof-cpu-aid.h
@@ -26,10 +26,8 @@ G_BEGIN_DECLS
 
 #define SYSPROF_TYPE_CPU_AID (sysprof_cpu_aid_get_type())
 
-SYSPROF_AVAILABLE_IN_ALL
 G_DECLARE_FINAL_TYPE (SysprofCpuAid, sysprof_cpu_aid, SYSPROF, CPU_AID, SysprofAid)
 
-SYSPROF_AVAILABLE_IN_ALL
 SysprofAid *sysprof_cpu_aid_new (void);
 
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-depth-visualizer.c b/src/libsysprof-ui/sysprof-depth-visualizer.c
new file mode 100644
index 0000000..08183eb
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-depth-visualizer.c
@@ -0,0 +1,430 @@
+/* sysprof-depth-visualizer.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-depth-visualizer"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "pointcache.h"
+#include "sysprof-depth-visualizer.h"
+
+struct _SysprofDepthVisualizer
+{
+  SysprofVisualizer     parent_instance;
+  SysprofCaptureReader *reader;
+  PointCache           *points;
+  guint                 reload_source;
+  guint                 mode;
+  GtkAllocation         last_alloc;
+};
+
+typedef struct
+{
+  SysprofCaptureReader *reader;
+  PointCache           *pc;
+  gint64                begin_time;
+  gint64                end_time;
+  gint64                duration;
+  guint                 max_n_addrs;
+  guint                 mode;
+} State;
+
+G_DEFINE_TYPE (SysprofDepthVisualizer, sysprof_depth_visualizer, SYSPROF_TYPE_VISUALIZER)
+
+static void
+state_free (State *st)
+{
+  g_clear_pointer (&st->reader, sysprof_capture_reader_unref);
+  g_clear_pointer (&st->pc, point_cache_unref);
+  g_slice_free (State, st);
+}
+
+static gboolean
+discover_max_n_addr (const SysprofCaptureFrame *frame,
+                     gpointer                   user_data)
+{
+  const SysprofCaptureSample *sample = (const SysprofCaptureSample *)frame;
+  State *st = user_data;
+
+  g_assert (frame != NULL);
+  g_assert (frame->type == SYSPROF_CAPTURE_FRAME_SAMPLE);
+  g_assert (st != NULL);
+
+  st->max_n_addrs = MAX (st->max_n_addrs, sample->n_addrs);
+
+  return TRUE;
+}
+
+static gboolean
+build_point_cache_cb (const SysprofCaptureFrame *frame,
+                      gpointer                   user_data)
+{
+  const SysprofCaptureSample *sample = (const SysprofCaptureSample *)frame;
+  State *st = user_data;
+  gdouble x, y;
+  gboolean has_kernel = FALSE;
+
+  g_assert (frame != NULL);
+  g_assert (frame->type == SYSPROF_CAPTURE_FRAME_SAMPLE);
+  g_assert (st != NULL);
+
+  x = (frame->time - st->begin_time) / (gdouble)st->duration;
+  y = sample->n_addrs / (gdouble)st->max_n_addrs;
+
+  /* If this contains a context-switch (meaning we're going into the kernel
+   * to do some work, use a negative value for Y so that we know later on
+   * that we should draw it with a different color (after removing the negation
+   * on the value.
+   *
+   * We skip past the first index, which is always a context switch as it is
+   * our perf handler.
+   */
+  for (guint i = 1; i < sample->n_addrs; i++)
+    {
+      SysprofAddressContext kind;
+
+      if (sysprof_address_is_context_switch (sample->addrs[i], &kind))
+        {
+          has_kernel = TRUE;
+          y = -y;
+          break;
+        }
+    }
+
+  if (!has_kernel)
+    point_cache_add_point_to_set (st->pc, 1, x, y);
+  else
+    point_cache_add_point_to_set (st->pc, 2, x, y);
+
+  return TRUE;
+}
+
+static void
+sysprof_depth_visualizer_worker (GTask        *task,
+                                 gpointer      source_object,
+                                 gpointer      task_data,
+                                 GCancellable *cancellable)
+{
+  static const SysprofCaptureFrameType types[] = { SYSPROF_CAPTURE_FRAME_SAMPLE, };
+  g_autoptr(SysprofCaptureCursor) cursor = NULL;
+  SysprofCaptureCondition *condition;
+  State *st = task_data;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (SYSPROF_IS_DEPTH_VISUALIZER (source_object));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (st->duration != 0)
+    {
+      cursor = sysprof_capture_cursor_new (st->reader);
+      condition = sysprof_capture_condition_new_where_type_in (G_N_ELEMENTS (types), types);
+      sysprof_capture_cursor_add_condition (cursor, g_steal_pointer (&condition));
+
+      sysprof_capture_cursor_foreach (cursor, discover_max_n_addr, st);
+      sysprof_capture_cursor_reset (cursor);
+      sysprof_capture_cursor_foreach (cursor, build_point_cache_cb, st);
+    }
+
+  g_task_return_pointer (task,
+                         g_steal_pointer (&st->pc),
+                         (GDestroyNotify) point_cache_unref);
+}
+
+static void
+apply_point_cache_cb (GObject      *object,
+                      GAsyncResult *result,
+                      gpointer      user_data)
+{
+  SysprofDepthVisualizer *self = (SysprofDepthVisualizer *)object;
+  PointCache *pc;
+
+  g_assert (SYSPROF_IS_DEPTH_VISUALIZER (self));
+  g_assert (G_IS_TASK (result));
+
+  if ((pc = g_task_propagate_pointer (G_TASK (result), NULL)))
+    {
+      g_clear_pointer (&self->points, point_cache_unref);
+      self->points = g_steal_pointer (&pc);
+      gtk_widget_queue_draw (GTK_WIDGET (self));
+    }
+}
+
+static void
+sysprof_depth_visualizer_reload (SysprofDepthVisualizer *self)
+{
+  g_autoptr(GTask) task = NULL;
+  GtkAllocation alloc;
+  State *st;
+
+  g_assert (SYSPROF_IS_DEPTH_VISUALIZER (self));
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  st = g_slice_new0 (State);
+  st->reader = sysprof_capture_reader_ref (self->reader);
+  st->pc = point_cache_new ();
+  st->max_n_addrs = 0;
+  st->begin_time = sysprof_capture_reader_get_start_time (self->reader);
+  st->end_time = sysprof_capture_reader_get_end_time (self->reader);
+  st->duration = st->end_time - st->begin_time;
+  st->mode = self->mode;
+
+  point_cache_add_set (st->pc, 1);
+  point_cache_add_set (st->pc, 2);
+
+  task = g_task_new (self, NULL, apply_point_cache_cb, NULL);
+  g_task_set_source_tag (task, sysprof_depth_visualizer_reload);
+  g_task_set_task_data (task, st, (GDestroyNotify) state_free);
+  g_task_run_in_thread (task, sysprof_depth_visualizer_worker);
+}
+
+static void
+sysprof_depth_visualizer_set_reader (SysprofVisualizer    *row,
+                                     SysprofCaptureReader *reader)
+{
+  SysprofDepthVisualizer *self = (SysprofDepthVisualizer *)row;
+
+  g_assert (SYSPROF_IS_DEPTH_VISUALIZER (self));
+
+  if (self->reader != reader)
+    {
+      if (self->reader != NULL)
+        {
+          sysprof_capture_reader_unref (self->reader);
+          self->reader = NULL;
+        }
+
+      if (reader != NULL)
+        {
+          self->reader = sysprof_capture_reader_ref (reader);
+          sysprof_depth_visualizer_reload (self);
+        }
+    }
+}
+
+static gboolean
+sysprof_depth_visualizer_draw (GtkWidget *widget,
+                               cairo_t   *cr)
+{
+  SysprofDepthVisualizer *self = (SysprofDepthVisualizer *)widget;
+  GtkAllocation alloc;
+  GdkRectangle clip;
+  const Point *points;
+  gboolean ret;
+  guint n_points = 0;
+  GdkRGBA user;
+  GdkRGBA system;
+
+  g_assert (SYSPROF_IS_DEPTH_VISUALIZER (self));
+  g_assert (cr != NULL);
+
+  ret = GTK_WIDGET_CLASS (sysprof_depth_visualizer_parent_class)->draw (widget, cr);
+
+  if (self->points == NULL)
+    return ret;
+
+  gdk_rgba_parse (&user, "#1a5fb4");
+  gdk_rgba_parse (&system, "#3584e4");
+
+  gtk_widget_get_allocation (widget, &alloc);
+
+  if (!gdk_cairo_get_clip_rectangle (cr, &clip))
+    return ret;
+
+  /* Draw user-space stacks */
+  if (self->mode != SYSPROF_DEPTH_VISUALIZER_KERNEL_ONLY &&
+      (points = point_cache_get_points (self->points, 1, &n_points)))
+    {
+      g_autofree SysprofVisualizerAbsolutePoint *out_points = NULL;
+
+      out_points = g_new (SysprofVisualizerAbsolutePoint, n_points);
+      sysprof_visualizer_translate_points (SYSPROF_VISUALIZER (widget),
+                                           (const SysprofVisualizerRelativePoint *)points,
+                                           n_points, out_points, n_points);
+
+      cairo_set_line_width (cr, 1.0);
+      gdk_cairo_set_source_rgba (cr, &user);
+
+      for (guint i = 0; i < n_points; i++)
+        {
+          gdouble x, y;
+
+          x = out_points[i].x;
+          y = out_points[i].y;
+
+          if (x < clip.x)
+            continue;
+
+          if (x > clip.x + clip.width)
+            break;
+
+          for (guint j = i + 1; j < n_points; j++)
+            {
+              if (out_points[j].x != x)
+                break;
+
+              y = MIN (y, out_points[j].y);
+            }
+
+          x += alloc.x;
+
+          cairo_move_to (cr, (guint)x + .5, alloc.height);
+          cairo_line_to (cr, (guint)x + .5, y);
+        }
+
+      cairo_stroke (cr);
+    }
+
+  /* Draw kernel-space stacks */
+  if (self->mode != SYSPROF_DEPTH_VISUALIZER_USER_ONLY &&
+      (points = point_cache_get_points (self->points, 2, &n_points)))
+    {
+      g_autofree SysprofVisualizerAbsolutePoint *out_points = NULL;
+
+      out_points = g_new (SysprofVisualizerAbsolutePoint, n_points);
+      sysprof_visualizer_translate_points (SYSPROF_VISUALIZER (widget),
+                                           (const SysprofVisualizerRelativePoint *)points,
+                                           n_points, out_points, n_points);
+
+      cairo_set_line_width (cr, 1.0);
+      gdk_cairo_set_source_rgba (cr, &system);
+
+      for (guint i = 0; i < n_points; i++)
+        {
+          gdouble x, y;
+
+          x = out_points[i].x;
+          y = out_points[i].y;
+
+          if (x < clip.x)
+            continue;
+
+          if (x > clip.x + clip.width)
+            break;
+
+          for (guint j = i + 1; j < n_points; j++)
+            {
+              if (out_points[j].x != x)
+                break;
+
+              y = MIN (y, out_points[j].y);
+            }
+
+          x += alloc.x;
+
+          cairo_move_to (cr, (guint)x + .5, alloc.height);
+          cairo_line_to (cr, (guint)x + .5, y);
+        }
+
+      cairo_stroke (cr);
+    }
+
+  return ret;
+}
+
+static gboolean
+sysprof_depth_visualizer_do_reload (gpointer data)
+{
+  SysprofDepthVisualizer *self = data;
+  self->reload_source = 0;
+  sysprof_depth_visualizer_reload (self);
+  return G_SOURCE_REMOVE;
+}
+
+static void
+sysprof_depth_visualizer_queue_reload (SysprofDepthVisualizer *self)
+{
+  g_assert (SYSPROF_IS_DEPTH_VISUALIZER (self));
+
+  if (self->reload_source)
+    g_source_remove (self->reload_source);
+
+  self->reload_source = gdk_threads_add_idle (sysprof_depth_visualizer_do_reload, self);
+}
+
+static void
+sysprof_depth_visualizer_size_allocate (GtkWidget     *widget,
+                                        GtkAllocation *alloc)
+{
+  SysprofDepthVisualizer *self = (SysprofDepthVisualizer *)widget;
+
+  GTK_WIDGET_CLASS (sysprof_depth_visualizer_parent_class)->size_allocate (widget, alloc);
+
+  if (alloc->width != self->last_alloc.x ||
+      alloc->height != self->last_alloc.height)
+    {
+      sysprof_depth_visualizer_queue_reload (SYSPROF_DEPTH_VISUALIZER (widget));
+      self->last_alloc = *alloc;
+    }
+}
+
+static void
+sysprof_depth_visualizer_finalize (GObject *object)
+{
+  SysprofDepthVisualizer *self = (SysprofDepthVisualizer *)object;
+
+  g_clear_pointer (&self->reader, sysprof_capture_reader_unref);
+
+  if (self->reload_source)
+    {
+      g_source_remove (self->reload_source);
+      self->reload_source = 0;
+    }
+
+  G_OBJECT_CLASS (sysprof_depth_visualizer_parent_class)->finalize (object);
+}
+
+static void
+sysprof_depth_visualizer_class_init (SysprofDepthVisualizerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  SysprofVisualizerClass *row_class = SYSPROF_VISUALIZER_CLASS (klass);
+
+  object_class->finalize = sysprof_depth_visualizer_finalize;
+
+  widget_class->draw = sysprof_depth_visualizer_draw;
+  widget_class->size_allocate = sysprof_depth_visualizer_size_allocate;
+
+  row_class->set_reader = sysprof_depth_visualizer_set_reader;
+}
+
+static void
+sysprof_depth_visualizer_init (SysprofDepthVisualizer *self)
+{
+}
+
+SysprofVisualizer *
+sysprof_depth_visualizer_new (SysprofDepthVisualizerMode  mode)
+{
+  SysprofDepthVisualizer *self;
+
+  g_return_val_if_fail (mode == SYSPROF_DEPTH_VISUALIZER_COMBINED ||
+                        mode == SYSPROF_DEPTH_VISUALIZER_KERNEL_ONLY ||
+                        mode == SYSPROF_DEPTH_VISUALIZER_USER_ONLY,
+                        NULL);
+
+  self = g_object_new (SYSPROF_TYPE_DEPTH_VISUALIZER, NULL);
+  self->mode = mode;
+
+  return SYSPROF_VISUALIZER (g_steal_pointer (&self));
+}
diff --git a/src/libsysprof-ui/sysprof-depth-visualizer.h b/src/libsysprof-ui/sysprof-depth-visualizer.h
new file mode 100644
index 0000000..425c4d4
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-depth-visualizer.h
@@ -0,0 +1,40 @@
+/* sysprof-depth-visualizer.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "sysprof-visualizer.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  SYSPROF_DEPTH_VISUALIZER_COMBINED,
+  SYSPROF_DEPTH_VISUALIZER_KERNEL_ONLY,
+  SYSPROF_DEPTH_VISUALIZER_USER_ONLY,
+} SysprofDepthVisualizerMode;
+
+#define SYSPROF_TYPE_DEPTH_VISUALIZER (sysprof_depth_visualizer_get_type())
+
+G_DECLARE_FINAL_TYPE (SysprofDepthVisualizer, sysprof_depth_visualizer, SYSPROF, DEPTH_VISUALIZER, 
SysprofVisualizer)
+
+SysprofVisualizer *sysprof_depth_visualizer_new (SysprofDepthVisualizerMode mode);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-details-page.c b/src/libsysprof-ui/sysprof-details-page.c
new file mode 100644
index 0000000..13170f4
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-details-page.c
@@ -0,0 +1,324 @@
+/* sysprof-details-page.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#ifndef _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+
+#define G_LOG_DOMAIN "sysprof-details-page"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <string.h>
+
+#include "sysprof-details-page.h"
+#include "sysprof-ui-private.h"
+
+#define NSEC_PER_SEC (G_USEC_PER_SEC * 1000L)
+
+struct _SysprofDetailsPage
+{
+  SysprofPage   parent_instance;
+
+  /* Template Objects */
+  DzlThreeGrid *three_grid;
+  GtkListStore *marks_store;
+  GtkTreeView  *marks_view;
+  GtkLabel     *counters;
+  GtkLabel     *duration;
+  GtkLabel     *filename;
+  GtkLabel     *forks;
+  GtkLabel     *marks;
+  GtkLabel     *processes;
+  GtkLabel     *samples;
+  GtkLabel     *start_time;
+  GtkLabel     *cpu_label;
+
+  guint         next_row;
+};
+
+G_DEFINE_TYPE (SysprofDetailsPage, sysprof_details_page, GTK_TYPE_BIN)
+
+#if GLIB_CHECK_VERSION(2, 56, 0)
+# define _g_date_time_new_from_iso8601 g_date_time_new_from_iso8601
+#else
+static GDateTime *
+_g_date_time_new_from_iso8601 (const gchar *str,
+                               GTimeZone   *default_tz)
+{
+  GTimeVal tv;
+
+  if (g_time_val_from_iso8601 (str, &tv))
+    {
+      g_autoptr(GDateTime) dt = g_date_time_new_from_timeval_utc (&tv);
+
+      if (default_tz)
+        return g_date_time_to_timezone (dt, default_tz);
+      else
+        return g_steal_pointer (&dt);
+    }
+
+  return NULL;
+}
+#endif
+
+static void
+sysprof_details_page_class_init (SysprofDetailsPageClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/sysprof/ui/sysprof-details-page.ui");
+  gtk_widget_class_bind_template_child (widget_class, SysprofDetailsPage, counters);
+  gtk_widget_class_bind_template_child (widget_class, SysprofDetailsPage, cpu_label);
+  gtk_widget_class_bind_template_child (widget_class, SysprofDetailsPage, duration);
+  gtk_widget_class_bind_template_child (widget_class, SysprofDetailsPage, filename);
+  gtk_widget_class_bind_template_child (widget_class, SysprofDetailsPage, forks);
+  gtk_widget_class_bind_template_child (widget_class, SysprofDetailsPage, marks);
+  gtk_widget_class_bind_template_child (widget_class, SysprofDetailsPage, marks_store);
+  gtk_widget_class_bind_template_child (widget_class, SysprofDetailsPage, marks_view);
+  gtk_widget_class_bind_template_child (widget_class, SysprofDetailsPage, processes);
+  gtk_widget_class_bind_template_child (widget_class, SysprofDetailsPage, samples);
+  gtk_widget_class_bind_template_child (widget_class, SysprofDetailsPage, start_time);
+  gtk_widget_class_bind_template_child (widget_class, SysprofDetailsPage, three_grid);
+
+  g_type_ensure (DZL_TYPE_THREE_GRID);
+}
+
+static void
+sysprof_details_page_init (SysprofDetailsPage *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_tree_selection_set_mode (gtk_tree_view_get_selection (self->marks_view),
+                               GTK_SELECTION_MULTIPLE);
+
+  self->next_row = 8;
+}
+
+GtkWidget *
+sysprof_details_page_new (void)
+{
+  return g_object_new (SYSPROF_TYPE_DETAILS_PAGE, NULL);
+}
+
+static void
+update_cpu_info_cb (GObject      *object,
+                    GAsyncResult *result,
+                    gpointer      user_data)
+{
+  g_autoptr(SysprofDetailsPage) self = user_data;
+  g_autofree gchar *str = NULL;
+
+  g_assert (SYSPROF_IS_DETAILS_PAGE (self));
+  g_assert (G_IS_TASK (result));
+
+  if ((str = g_task_propagate_pointer (G_TASK (result), NULL)))
+    gtk_label_set_label (self->cpu_label, str);
+}
+
+static gboolean
+cpu_info_cb (const SysprofCaptureFrame *frame,
+             gpointer                   user_data)
+{
+  const SysprofCaptureFileChunk *fc = (gpointer)frame;
+  const gchar *endptr;
+  const gchar *line;
+  gchar **str = user_data;
+
+  endptr = (gchar *)fc->data + fc->len;
+  line = memmem ((gchar *)fc->data, fc->len, "model name", 10);
+  endptr = memchr (line, '\n', endptr - line);
+
+  if (endptr)
+    {
+      gchar *tmp = *str = g_strndup (line, endptr - line);
+      for (; *tmp && *tmp != ':'; tmp++)
+        *tmp = ' ';
+      if (*tmp == ':')
+        *tmp = ' ';
+      g_strstrip (*str);
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+static void
+sysprof_details_page_update_cpu_info_worker (GTask        *task,
+                                             gpointer      source_object,
+                                             gpointer      task_data,
+                                             GCancellable *cancellable)
+{
+  SysprofCaptureCursor *cursor = task_data;
+  gchar *str = NULL;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (cursor != NULL);
+
+  sysprof_capture_cursor_foreach (cursor, cpu_info_cb, &str);
+  g_task_return_pointer (task, g_steal_pointer (&str), g_free);
+}
+
+static void
+sysprof_details_page_update_cpu_info (SysprofDetailsPage   *self,
+                                      SysprofCaptureReader *reader)
+{
+  g_autoptr(SysprofCaptureCursor) cursor = NULL;
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (SYSPROF_IS_DETAILS_PAGE (self));
+  g_assert (reader != NULL);
+
+  cursor = sysprof_capture_cursor_new (reader);
+  sysprof_capture_cursor_add_condition (cursor,
+                                        sysprof_capture_condition_new_where_file ("/proc/cpuinfo"));
+
+  task = g_task_new (NULL, NULL, update_cpu_info_cb, g_object_ref (self));
+  g_task_set_task_data (task,
+                        g_steal_pointer (&cursor),
+                        (GDestroyNotify) sysprof_capture_cursor_unref);
+  g_task_run_in_thread (task, sysprof_details_page_update_cpu_info_worker);
+}
+
+void
+sysprof_details_page_set_reader (SysprofDetailsPage   *self,
+                                 SysprofCaptureReader *reader)
+{
+  g_autoptr(GDateTime) dt = NULL;
+  g_autoptr(GDateTime) local = NULL;
+  g_autofree gchar *duration_str = NULL;
+  const gchar *filename;
+  const gchar *capture_at;
+  SysprofCaptureStat st_buf;
+  gint64 duration;
+
+  g_return_if_fail (SYSPROF_IS_DETAILS_PAGE (self));
+  g_return_if_fail (reader != NULL);
+
+  sysprof_details_page_update_cpu_info (self, reader);
+
+  if (!(filename = sysprof_capture_reader_get_filename (reader)))
+    filename = _("Memory Capture");
+
+  gtk_label_set_label (self->filename, filename);
+
+  if ((capture_at = sysprof_capture_reader_get_time (reader)) &&
+      (dt = _g_date_time_new_from_iso8601 (capture_at, NULL)) &&
+      (local = g_date_time_to_local (dt)))
+    {
+      g_autofree gchar *str = g_date_time_format (local, "%x %X");
+      gtk_label_set_label (self->start_time, str);
+    }
+
+  duration = sysprof_capture_reader_get_end_time (reader) -
+             sysprof_capture_reader_get_start_time (reader);
+  duration_str = g_strdup_printf (_("%0.4lf seconds"), duration / (gdouble)NSEC_PER_SEC);
+  gtk_label_set_label (self->duration, duration_str);
+
+  if (sysprof_capture_reader_get_stat (reader, &st_buf))
+    {
+#define SET_FRAME_COUNT(field, TYPE) \
+      G_STMT_START { \
+        g_autofree gchar *str = NULL; \
+        str = g_strdup_printf ("%"G_GSIZE_FORMAT, st_buf.frame_count[TYPE]); \
+        gtk_label_set_label (self->field, str); \
+      } G_STMT_END
+
+      SET_FRAME_COUNT (samples, SYSPROF_CAPTURE_FRAME_SAMPLE);
+      SET_FRAME_COUNT (marks, SYSPROF_CAPTURE_FRAME_MARK);
+      SET_FRAME_COUNT (processes, SYSPROF_CAPTURE_FRAME_PROCESS);
+      SET_FRAME_COUNT (forks, SYSPROF_CAPTURE_FRAME_FORK);
+      SET_FRAME_COUNT (counters, SYSPROF_CAPTURE_FRAME_CTRSET);
+
+#undef SET_FRAME_COUNT
+    }
+}
+
+void
+sysprof_details_page_add_item (SysprofDetailsPage *self,
+                               GtkWidget          *left,
+                               GtkWidget          *center)
+{
+  g_return_if_fail (SYSPROF_IS_DETAILS_PAGE (self));
+  g_return_if_fail (!left || GTK_IS_WIDGET (left));
+  g_return_if_fail (!center || GTK_IS_WIDGET (center));
+
+  if (left)
+    gtk_container_add_with_properties (GTK_CONTAINER (self->three_grid), left,
+                                       "row", self->next_row,
+                                       "column", DZL_THREE_GRID_COLUMN_LEFT,
+                                       NULL);
+
+  if (center)
+    gtk_container_add_with_properties (GTK_CONTAINER (self->three_grid), center,
+                                       "row", self->next_row,
+                                       "column", DZL_THREE_GRID_COLUMN_CENTER,
+                                       NULL);
+
+  self->next_row++;
+}
+
+void
+sysprof_details_page_add_mark (SysprofDetailsPage *self,
+                               const gchar        *mark,
+                               gint64              min,
+                               gint64              max,
+                               gint64              avg,
+                               gint64              hits)
+{
+  GtkTreeIter iter;
+
+  g_return_if_fail (SYSPROF_IS_DETAILS_PAGE (self));
+
+  gtk_list_store_append (self->marks_store, &iter);
+  gtk_list_store_set (self->marks_store, &iter,
+                      0, mark,
+                      1, min ? _sysprof_format_duration (min) : "—",
+                      2, max ? _sysprof_format_duration (max) : "—",
+                      3, avg ? _sysprof_format_duration (avg) : "—",
+                      4, hits,
+                      -1);
+}
+
+void
+sysprof_details_page_add_marks  (SysprofDetailsPage    *self,
+                                 const SysprofMarkStat *marks,
+                                 guint                  n_marks)
+{
+  g_return_if_fail (SYSPROF_IS_DETAILS_PAGE (self));
+  g_return_if_fail (marks != NULL || n_marks == 0);
+
+  if (marks == NULL || n_marks == 0)
+    return;
+
+  /* Be reasonable */
+  if (n_marks > 100)
+    n_marks = 100;
+
+  for (guint i = 0; i < n_marks; i++)
+    sysprof_details_page_add_mark (self,
+                                   marks[i].name,
+                                   marks[i].min,
+                                   marks[i].max,
+                                   marks[i].avg,
+                                   marks[i].count);
+}
diff --git a/src/libsysprof-ui/sysprof-details-page.h b/src/libsysprof-ui/sysprof-details-page.h
new file mode 100644
index 0000000..251bbe5
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-details-page.h
@@ -0,0 +1,60 @@
+/* sysprof-details-page.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <sysprof-capture.h>
+
+G_BEGIN_DECLS
+
+SYSPROF_ALIGNED_BEGIN (8)
+typedef struct
+{
+  gchar    name[152];
+  guint64  count;
+  gint64   max;
+  gint64   min;
+  gint64   avg;
+  guint64  avg_count;
+} SysprofMarkStat
+SYSPROF_ALIGNED_END (8);
+
+#define SYSPROF_TYPE_DETAILS_PAGE (sysprof_details_page_get_type())
+
+G_DECLARE_FINAL_TYPE (SysprofDetailsPage, sysprof_details_page, SYSPROF, DETAILS_PAGE, GtkBin)
+
+GtkWidget *sysprof_details_page_new        (void);
+void       sysprof_details_page_set_reader (SysprofDetailsPage    *self,
+                                            SysprofCaptureReader  *reader);
+void       sysprof_details_page_add_marks  (SysprofDetailsPage    *self,
+                                            const SysprofMarkStat *marks,
+                                            guint                  n_marks);
+void       sysprof_details_page_add_mark   (SysprofDetailsPage    *self,
+                                            const gchar           *mark,
+                                            gint64                 min,
+                                            gint64                 max,
+                                            gint64                 avg,
+                                            gint64                 hits);
+void       sysprof_details_page_add_item   (SysprofDetailsPage    *self,
+                                            GtkWidget             *left,
+                                            GtkWidget             *center);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-details-page.ui b/src/libsysprof-ui/sysprof-details-page.ui
new file mode 100644
index 0000000..c7373a3
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-details-page.ui
@@ -0,0 +1,372 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.0 -->
+<interface>
+  <requires lib="gtk+" version="3.22"/>
+  <template class="SysprofDetailsPage" parent="GtkBin">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="hscrollbar-policy">never</property>
+        <property name="propagate-natural-height">true</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="DzlThreeGrid" id="three_grid">
+            <property name="margin">36</property>
+            <property name="column-spacing">12</property>
+            <property name="row-spacing">6</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">true</property>
+                <property name="label" translatable="yes">Filename</property>
+                <property name="xalign">1</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">left</property>
+                <property name="row">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">true</property>
+                <property name="label" translatable="yes">Captured at</property>
+                <property name="xalign">1</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">left</property>
+                <property name="row">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">Duration</property>
+                <property name="xalign">1</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">left</property>
+                <property name="row">2</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">CPU Model</property>
+                <property name="xalign">1</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">left</property>
+                <property name="row">3</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="margin-top">12</property>
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Samples Captured</property>
+                <property name="xalign">1</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">left</property>
+                <property name="row">4</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Marks Captured</property>
+                <property name="xalign">1</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">left</property>
+                <property name="row">5</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Processes Captured</property>
+                <property name="xalign">1</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">left</property>
+                <property name="row">6</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Forks Captured</property>
+                <property name="xalign">1</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">left</property>
+                <property name="row">7</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="margin-bottom">12</property>
+                <property name="label" translatable="yes">Counters Captured</property>
+                <property name="xalign">1</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="column">left</property>
+                <property name="row">8</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="filename">
+                <property name="width-chars">35</property>
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="ellipsize">start</property>
+                <property name="xalign">0</property>
+                <property name="selectable">True</property>
+              </object>
+              <packing>
+                <property name="column">1</property>
+                <property name="row">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="start_time">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="ellipsize">start</property>
+                <property name="xalign">0</property>
+                <property name="selectable">True</property>
+              </object>
+              <packing>
+                <property name="column">center</property>
+                <property name="row">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="duration">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">0</property>
+                <property name="selectable">True</property>
+              </object>
+              <packing>
+                <property name="column">center</property>
+                <property name="row">2</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="cpu_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="ellipsize">end</property>
+                <property name="xalign">0</property>
+                <property name="selectable">True</property>
+              </object>
+              <packing>
+                <property name="column">center</property>
+                <property name="row">3</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="samples">
+                <property name="margin-top">12</property>
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="ellipsize">start</property>
+                <property name="xalign">0</property>
+                <property name="selectable">True</property>
+              </object>
+              <packing>
+                <property name="column">center</property>
+                <property name="row">4</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="marks">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="ellipsize">start</property>
+                <property name="xalign">0</property>
+                <property name="selectable">True</property>
+              </object>
+              <packing>
+                <property name="column">center</property>
+                <property name="row">5</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="processes">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="ellipsize">start</property>
+                <property name="xalign">0</property>
+                <property name="selectable">True</property>
+              </object>
+              <packing>
+                <property name="column">center</property>
+                <property name="row">6</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="forks">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="ellipsize">start</property>
+                <property name="xalign">0</property>
+                <property name="selectable">True</property>
+              </object>
+              <packing>
+                <property name="column">center</property>
+                <property name="row">7</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="counters">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="ellipsize">start</property>
+                <property name="margin-bottom">12</property>
+                <property name="xalign">0</property>
+                <property name="selectable">True</property>
+              </object>
+              <packing>
+                <property name="column">center</property>
+                <property name="row">8</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkFrame">
+                <property name="visible">True</property>
+                <property name="shadow-type">in</property>
+                <property name="margin-bottom">12</property>
+                <child>
+                  <object class="GtkTreeView" id="marks_view">
+                    <property name="model">marks_store</property>
+                    <property name="width-request">500</property>
+                    <property name="height-request">100</property>
+                    <property name="enable-grid-lines">both</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkTreeViewColumn">
+                        <property name="expand">true</property>
+                        <property name="title" translatable="yes">Mark</property>
+                        <child>
+                          <object class="GtkCellRendererText">
+                            <property name="xalign">0.0</property>
+                          </object>
+                          <attributes>
+                            <attribute name="text">0</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn">
+                        <property name="title" translatable="yes">Hits</property>
+                        <child>
+                          <object class="GtkCellRendererText">
+                            <property name="xalign">0.0</property>
+                          </object>
+                          <attributes>
+                            <attribute name="text">4</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn">
+                        <property name="title" translatable="yes">Min</property>
+                        <child>
+                          <object class="GtkCellRendererText">
+                            <property name="xalign">0.0</property>
+                          </object>
+                          <attributes>
+                            <attribute name="text">1</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn">
+                        <property name="title" translatable="yes">Max</property>
+                        <child>
+                          <object class="GtkCellRendererText">
+                            <property name="xalign">0.0</property>
+                          </object>
+                          <attributes>
+                            <attribute name="text">2</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn">
+                        <property name="title" translatable="yes">Avg</property>
+                        <child>
+                          <object class="GtkCellRendererText">
+                            <property name="xalign">0.0</property>
+                          </object>
+                          <attributes>
+                            <attribute name="text">3</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="column">center</property>
+                <property name="row">9</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkListStore" id="marks_store">
+    <columns>
+      <!-- column-name Mark -->
+      <column type="gchararray"/>
+      <!-- column-name Min -->
+      <column type="gchararray"/>
+      <!-- column-name Max -->
+      <column type="gchararray"/>
+      <!-- column-name Avg -->
+      <column type="gchararray"/>
+      <!-- column-name Hits -->
+      <column type="gint64"/>
+    </columns>
+  </object>
+</interface>
diff --git a/src/libsysprof-ui/sysprof-display.c b/src/libsysprof-ui/sysprof-display.c
index 7f31a54..d9dbf24 100644
--- a/src/libsysprof-ui/sysprof-display.c
+++ b/src/libsysprof-ui/sysprof-display.c
@@ -22,31 +22,49 @@
 
 #include "config.h"
 
+#include <dazzle.h>
 #include <glib/gi18n.h>
 
-#include <sysprof-capture.h>
-#include <sysprof-ui.h>
-#include <sysprof.h>
-
-#include "sysprof-profiler-assistant.h"
-#include "sysprof-capture-view.h"
+#include "sysprof-details-page.h"
 #include "sysprof-display.h"
-#include "sysprof-empty-state-view.h"
+#include "sysprof-profiler-assistant.h"
+#include "sysprof-failed-state-view.h"
 #include "sysprof-recording-state-view.h"
+#include "sysprof-theme-manager.h"
 #include "sysprof-ui-private.h"
+#include "sysprof-visualizers-frame.h"
+#include "sysprof-visualizer-group-private.h"
+
+#include "sysprof-battery-aid.h"
+#include "sysprof-callgraph-aid.h"
+#include "sysprof-counters-aid.h"
+#include "sysprof-cpu-aid.h"
+#include "sysprof-logs-aid.h"
+#include "sysprof-marks-aid.h"
+
+typedef enum
+{
+  SYSPROF_CAPTURE_FLAGS_CAN_REPLAY = 1 << 1,
+} SysprofCaptureFlags;
 
 typedef struct
 {
+  SysprofCaptureReader      *reader;
+  SysprofCaptureCondition   *filter;
   GFile                     *file;
   SysprofProfiler           *profiler;
   GError                    *error;
 
-  /* Template Objects */
+  /* Template Widgets */
+  SysprofVisualizersFrame   *visualizers;
+  GtkStack                  *pages;
+  SysprofDetailsPage        *details;
+  GtkStack                  *stack;
   SysprofProfilerAssistant  *assistant;
-  SysprofCaptureView        *capture_view;
-  SysprofEmptyStateView     *failed_view;
   SysprofRecordingStateView *recording_view;
-  GtkStack                  *stack;
+  SysprofFailedStateView    *failed_view;
+
+  SysprofCaptureFlags        flags;
 } SysprofDisplayPrivate;
 
 G_DEFINE_TYPE_WITH_PRIVATE (SysprofDisplay, sysprof_display, GTK_TYPE_BIN)
@@ -57,43 +75,29 @@ enum {
   PROP_CAN_SAVE,
   PROP_RECORDING,
   PROP_TITLE,
+  PROP_VISIBLE_PAGE,
   N_PROPS
 };
 
 static GParamSpec *properties [N_PROPS];
 
-/**
- * sysprof_display_new:
- *
- * Create a new #SysprofDisplay.
- *
- * Returns: (transfer full): a newly created #SysprofDisplay
- *
- * Since: 3.34
- */
-GtkWidget *
-sysprof_display_new (void)
-{
-  return g_object_new (SYSPROF_TYPE_DISPLAY, NULL);
-}
-
 static void
-sysprof_display_load_cb (SysprofCaptureView *view,
-                         GAsyncResult       *result,
-                         gpointer            user_data)
+update_title_child_property (SysprofDisplay *self)
 {
-  g_autoptr(SysprofDisplay) self = user_data;
-  g_autoptr(GError) error = NULL;
+  GtkWidget *parent;
 
-  g_assert (SYSPROF_IS_CAPTURE_VIEW (view));
-  g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (SYSPROF_IS_DISPLAY (self));
 
-  if (!sysprof_capture_view_load_finish (view, result, &error))
-    g_warning ("Failed to load capture: %s", error->message);
+  if ((parent = gtk_widget_get_parent (GTK_WIDGET (self))) && GTK_IS_NOTEBOOK (parent))
+    {
+      g_autofree gchar *title = sysprof_display_dup_title (self);
 
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CAN_REPLAY]);
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CAN_SAVE]);
+      gtk_container_child_set (GTK_CONTAINER (parent), GTK_WIDGET (self),
+                               "menu-label", title,
+                               NULL);
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
 }
 
 static void
@@ -141,12 +145,10 @@ sysprof_display_profiler_stopped_cb (SysprofDisplay  *self,
           goto notify;
         }
 
-      sysprof_capture_view_load_async (priv->capture_view,
-                                       reader,
-                                       NULL,
-                                       (GAsyncReadyCallback) sysprof_display_load_cb,
-                                       g_object_ref (self));
-      gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (priv->capture_view));
+      sysprof_display_load_async (self,
+                                  reader,
+                                  NULL, NULL, NULL);
+      gtk_stack_set_visible_child_name (priv->stack, "view");
     }
 
 notify:
@@ -201,11 +203,20 @@ sysprof_display_start_recording_cb (SysprofDisplay           *self,
   sysprof_profiler_start (profiler);
 }
 
+static gboolean
+sysprof_display_get_is_recording (SysprofDisplay *self)
+{
+  SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
+
+  g_assert (SYSPROF_IS_DISPLAY (self));
+
+  return GTK_WIDGET (priv->recording_view) == gtk_stack_get_visible_child (priv->stack);
+}
+
 gchar *
 sysprof_display_dup_title (SysprofDisplay *self)
 {
   SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
-  SysprofCaptureReader *reader;
 
   g_return_val_if_fail (SYSPROF_IS_DISPLAY (self), NULL);
 
@@ -221,44 +232,100 @@ sysprof_display_dup_title (SysprofDisplay *self)
   if (priv->file != NULL)
     return g_file_get_basename (priv->file);
 
-  if ((reader = sysprof_capture_view_get_reader (priv->capture_view)))
+  if (priv->reader != NULL)
     {
       const gchar *filename;
 
-      if ((filename = sysprof_capture_reader_get_filename (reader)))
+      if ((filename = sysprof_capture_reader_get_filename (priv->reader)))
         return g_path_get_basename (filename);
     }
 
   return g_strdup (_("New Recording"));
 }
 
+/**
+ * sysprof_display_new:
+ *
+ * Create a new #SysprofDisplay.
+ *
+ * Returns: (transfer full): a newly created #SysprofDisplay
+ */
+GtkWidget *
+sysprof_display_new (void)
+{
+  return g_object_new (SYSPROF_TYPE_DISPLAY, NULL);
+}
+
 static void
-update_title_child_property (SysprofDisplay *self)
+sysprof_display_notify_selection_cb (SysprofDisplay          *self,
+                                     GParamSpec              *pspec,
+                                     SysprofVisualizersFrame *visualizers)
 {
-  GtkWidget *parent;
+  SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
+  SysprofSelection *selection;
 
   g_assert (SYSPROF_IS_DISPLAY (self));
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (visualizers));
 
-  if ((parent = gtk_widget_get_parent (GTK_WIDGET (self))) && GTK_IS_NOTEBOOK (parent))
+  g_clear_pointer (&priv->filter, sysprof_capture_condition_unref);
+
+  if ((selection = sysprof_visualizers_frame_get_selection (visualizers)))
     {
-      g_autofree gchar *title = sysprof_display_dup_title (self);
+      SysprofCaptureCondition *cond = NULL;
+      guint n_ranges = sysprof_selection_get_n_ranges (selection);
 
-      gtk_container_child_set (GTK_CONTAINER (parent), GTK_WIDGET (self),
-                               "menu-label", title,
-                               NULL);
-    }
+      for (guint i = 0; i < n_ranges; i++)
+        {
+          SysprofCaptureCondition *c;
+          gint64 begin, end;
 
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+          sysprof_selection_get_nth_range (selection, i, &begin, &end);
+          c = sysprof_capture_condition_new_where_time_between (begin, end);
+
+          if (cond == NULL)
+            cond = c;
+          else
+            cond = sysprof_capture_condition_new_or (cond, c);
+        }
+
+      priv->filter = cond;
+
+      /* Opportunistically load pages */
+      if (priv->reader != NULL)
+        {
+          GList *pages = gtk_container_get_children (GTK_CONTAINER (priv->pages));
+
+          for (const GList *iter = pages; iter; iter = iter->next)
+            {
+              if (SYSPROF_IS_PAGE (iter->data))
+                sysprof_page_load_async (iter->data,
+                                         priv->reader,
+                                         selection,
+                                         priv->filter,
+                                         NULL, NULL, NULL);
+            }
+
+          g_list_free (pages);
+        }
+    }
 }
 
-static gboolean
-sysprof_display_get_is_recording (SysprofDisplay *self)
+static void
+change_page_cb (GSimpleAction *action,
+                GVariant      *param,
+                gpointer       user_data)
 {
+  SysprofDisplay *self = user_data;
   SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
 
-  g_assert (SYSPROF_IS_DISPLAY (self));
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (param != NULL);
 
-  return GTK_WIDGET (priv->recording_view) == gtk_stack_get_visible_child (priv->stack);
+  if (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING))
+    {
+      const gchar *str = g_variant_get_string (param, NULL);
+      gtk_stack_set_visible_child_name (priv->pages, str);
+    }
 }
 
 static void
@@ -274,27 +341,14 @@ stop_recording_cb (GSimpleAction *action,
   sysprof_display_stop_recording (self);
 }
 
-static void
-sysprof_display_parent_set (GtkWidget *widget,
-                            GtkWidget *old_parent)
-{
-  g_assert (SYSPROF_IS_DISPLAY (widget));
-
-  if (GTK_WIDGET_CLASS (sysprof_display_parent_class)->parent_set)
-    GTK_WIDGET_CLASS (sysprof_display_parent_class)->parent_set (widget, old_parent);
-
-  update_title_child_property (SYSPROF_DISPLAY (widget));
-}
-
 static void
 sysprof_display_finalize (GObject *object)
 {
   SysprofDisplay *self = (SysprofDisplay *)object;
   SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
 
-  g_clear_error (&priv->error);
-  g_clear_object (&priv->profiler);
-  g_clear_object (&priv->file);
+  g_clear_pointer (&priv->reader, sysprof_capture_reader_unref);
+  g_clear_pointer (&priv->filter, sysprof_capture_condition_unref);
 
   G_OBJECT_CLASS (sysprof_display_parent_class)->finalize (object);
 }
@@ -305,7 +359,7 @@ sysprof_display_get_property (GObject    *object,
                               GValue     *value,
                               GParamSpec *pspec)
 {
-  SysprofDisplay *self = (SysprofDisplay *)object;
+  SysprofDisplay *self = SYSPROF_DISPLAY (object);
 
   switch (prop_id)
     {
@@ -325,6 +379,29 @@ sysprof_display_get_property (GObject    *object,
       g_value_take_string (value, sysprof_display_dup_title (self));
       break;
 
+    case PROP_VISIBLE_PAGE:
+      g_value_set_object (value, sysprof_display_get_visible_page (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+sysprof_display_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  SysprofDisplay *self = SYSPROF_DISPLAY (object);
+
+  switch (prop_id)
+    {
+    case PROP_VISIBLE_PAGE:
+      sysprof_display_set_visible_page (self, g_value_get_object (value));
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
@@ -338,8 +415,22 @@ sysprof_display_class_init (SysprofDisplayClass *klass)
 
   object_class->finalize = sysprof_display_finalize;
   object_class->get_property = sysprof_display_get_property;
+  object_class->set_property = sysprof_display_set_property;
+
+  sysprof_theme_manager_register_resource (sysprof_theme_manager_get_default (),
+                                           NULL,
+                                           NULL,
+                                           "/org/gnome/sysprof/css/SysprofDisplay-shared.css");
 
-  widget_class->parent_set = sysprof_display_parent_set;
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/sysprof/ui/sysprof-display.ui");
+  gtk_widget_class_set_css_name (widget_class, "SysprofDisplay");
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofDisplay, assistant);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofDisplay, details);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofDisplay, failed_view);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofDisplay, pages);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofDisplay, recording_view);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofDisplay, stack);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofDisplay, visualizers);
 
   properties [PROP_CAN_REPLAY] =
     g_param_spec_boolean ("can-replay",
@@ -367,20 +458,23 @@ sysprof_display_class_init (SysprofDisplayClass *klass)
                          "Title",
                          "The title of the display",
                          NULL,
-                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
-  g_object_class_install_properties (object_class, N_PROPS, properties);
+  properties [PROP_VISIBLE_PAGE] =
+    g_param_spec_object ("visible-page",
+                         "Visible Page",
+                         "Visible Page",
+                         SYSPROF_TYPE_PAGE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
-  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/sysprof/ui/sysprof-display.ui");
-  gtk_widget_class_bind_template_child_private (widget_class, SysprofDisplay, capture_view);
-  gtk_widget_class_bind_template_child_private (widget_class, SysprofDisplay, failed_view);
-  gtk_widget_class_bind_template_child_private (widget_class, SysprofDisplay, recording_view);
-  gtk_widget_class_bind_template_child_private (widget_class, SysprofDisplay, assistant);
-  gtk_widget_class_bind_template_child_private (widget_class, SysprofDisplay, stack);
+  g_object_class_install_properties (object_class, N_PROPS, properties);
 
-  g_type_ensure (SYSPROF_TYPE_CAPTURE_VIEW);
-  g_type_ensure (SYSPROF_TYPE_EMPTY_STATE_VIEW);
+  g_type_ensure (DZL_TYPE_MULTI_PANED);
+  g_type_ensure (SYSPROF_TYPE_DETAILS_PAGE);
+  g_type_ensure (SYSPROF_TYPE_FAILED_STATE_VIEW);
+  g_type_ensure (SYSPROF_TYPE_PROFILER_ASSISTANT);
   g_type_ensure (SYSPROF_TYPE_RECORDING_STATE_VIEW);
+  g_type_ensure (SYSPROF_TYPE_VISUALIZERS_FRAME);
 }
 
 static void
@@ -388,202 +482,561 @@ sysprof_display_init (SysprofDisplay *self)
 {
   SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
   g_autoptr(GSimpleActionGroup) group = g_simple_action_group_new ();
-  static const GActionEntry actions[] = {
+  static GActionEntry entries[] = {
+    { "page", change_page_cb, "s" },
     { "stop-recording", stop_recording_cb },
   };
+  g_autoptr(GPropertyAction) page = NULL;
 
   gtk_widget_init_template (GTK_WIDGET (self));
 
-  g_action_map_add_action_entries (G_ACTION_MAP (group),
-                                   actions,
-                                   G_N_ELEMENTS (actions),
-                                   self);
-  gtk_widget_insert_action_group (GTK_WIDGET (self), "display", G_ACTION_GROUP (group));
-
   g_signal_connect_object (priv->assistant,
                            "start-recording",
                            G_CALLBACK (sysprof_display_start_recording_cb),
                            self,
                            G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->visualizers,
+                           "notify::selection",
+                           G_CALLBACK (sysprof_display_notify_selection_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  page = g_property_action_new ("page", priv->pages, "visible-child-name");
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   entries,
+                                   G_N_ELEMENTS (entries),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "display", G_ACTION_GROUP (group));
 }
 
-/**
- * sysprof_display_get_profiler:
- *
- * Gets the proflier for the display.
- *
- * Returns: (transfer none) (nullable): a #SysprofProfiler or %NULL
- *
- * Since: 3.34
- */
-SysprofProfiler *
-sysprof_display_get_profiler (SysprofDisplay *self)
+void
+sysprof_display_add_group (SysprofDisplay         *self,
+                           SysprofVisualizerGroup *group)
 {
   SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
 
+  g_return_if_fail (SYSPROF_IS_DISPLAY (self));
+  g_return_if_fail (SYSPROF_IS_VISUALIZER_GROUP (group));
+
+  if (priv->reader != NULL)
+    _sysprof_visualizer_group_set_reader (group, priv->reader);
+
+  gtk_container_add (GTK_CONTAINER (priv->visualizers), GTK_WIDGET (group));
+}
+
+void
+sysprof_display_add_page (SysprofDisplay *self,
+                          SysprofPage    *page)
+{
+  SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
+  SysprofSelection *selection;
+  const gchar *title;
+
+  g_return_if_fail (SYSPROF_IS_DISPLAY (self));
+  g_return_if_fail (SYSPROF_IS_PAGE (page));
+
+  title = sysprof_page_get_title (page);
+
+  gtk_container_add_with_properties (GTK_CONTAINER (priv->pages), GTK_WIDGET (page),
+                                     "title", title,
+                                     NULL);
+
+  selection = sysprof_visualizers_frame_get_selection (priv->visualizers);
+
+  sysprof_page_set_size_group (page,
+                               sysprof_visualizers_frame_get_size_group (priv->visualizers));
+
+  sysprof_page_set_hadjustment (page,
+                                sysprof_visualizers_frame_get_hadjustment (priv->visualizers));
+
+  if (priv->reader != NULL)
+    sysprof_page_load_async (page,
+                             priv->reader,
+                             selection,
+                             priv->filter,
+                             NULL, NULL, NULL);
+}
+
+SysprofPage *
+sysprof_display_get_visible_page (SysprofDisplay *self)
+{
+  SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
+  GtkWidget *visible_page;
+
   g_return_val_if_fail (SYSPROF_IS_DISPLAY (self), NULL);
 
-  return priv->profiler;
+  visible_page = gtk_stack_get_visible_child (priv->pages);
+
+  if (SYSPROF_IS_PAGE (visible_page))
+    return SYSPROF_PAGE (visible_page);
+
+  return NULL;
 }
 
-/**
- * sysprof_display_is_empty:
- *
- * Checks if any content is or will be loaded into @self.
- *
- * Returns: %TRUE if the tab is unperterbed.
- *
- * Since: 3.34
- */
-gboolean
-sysprof_display_is_empty (SysprofDisplay *self)
+void
+sysprof_display_set_visible_page (SysprofDisplay *self,
+                                  SysprofPage    *page)
 {
   SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
 
-  g_return_val_if_fail (SYSPROF_IS_DISPLAY (self), FALSE);
+  g_return_if_fail (SYSPROF_IS_DISPLAY (self));
+  g_return_if_fail (SYSPROF_IS_PAGE (page));
 
-  return priv->file == NULL &&
-         priv->profiler == NULL &&
-         gtk_stack_get_visible_child (priv->stack) == GTK_WIDGET (priv->assistant) &&
-         NULL == sysprof_capture_view_get_reader (priv->capture_view);
+  gtk_stack_set_visible_child (priv->pages, GTK_WIDGET (page));
 }
 
 static void
-sysprof_display_open_cb (GObject      *object,
-                         GAsyncResult *result,
-                         gpointer      user_data)
+sysprof_display_present_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
 {
-  g_autoptr(SysprofDisplay) self = user_data;
-  SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
-  g_autoptr(SysprofCaptureReader) reader = NULL;
+  SysprofAid *aid = (SysprofAid *)object;
+  g_autoptr(GTask) task = user_data;
   g_autoptr(GError) error = NULL;
+  guint *n_active;
 
-  g_assert (SYSPROF_IS_DISPLAY (self));
-  g_assert (G_IS_TASK (result));
+  g_assert (SYSPROF_IS_AID (aid));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!sysprof_aid_present_finish (aid, result, &error))
+    g_warning ("Failed to present aid %s: %s", G_OBJECT_TYPE_NAME (aid), error->message);
+
+  n_active = g_task_get_task_data (task);
+
+  (*n_active)--;
+
+  if (n_active == 0)
+    g_task_return_boolean (task, TRUE);
+}
+
+static void
+sysprof_display_present_async (SysprofDisplay       *self,
+                               SysprofCaptureReader *reader,
+                               GCancellable         *cancellable,
+                               GAsyncReadyCallback   callback,
+                               gpointer              user_data)
+{
+  g_autoptr(GPtrArray) aids = NULL;
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (SYSPROF_IS_DISPLAY (self));
+  g_return_if_fail (reader != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  if (!(reader = g_task_propagate_pointer (G_TASK (result), &error)))
+  aids = g_ptr_array_new_with_free_func (g_object_unref);
+  g_ptr_array_add (aids, sysprof_battery_aid_new ());
+  g_ptr_array_add (aids, sysprof_counters_aid_new ());
+  g_ptr_array_add (aids, sysprof_cpu_aid_new ());
+  g_ptr_array_add (aids, sysprof_callgraph_aid_new ());
+  g_ptr_array_add (aids, sysprof_logs_aid_new ());
+  g_ptr_array_add (aids, sysprof_marks_aid_new ());
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_display_present_async);
+
+  if (aids->len == 0)
     {
-      gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (priv->failed_view));
+      g_task_return_boolean (task, TRUE);
       return;
     }
 
-  sysprof_capture_view_load_async (priv->capture_view,
-                                   reader,
-                                   NULL,
-                                   (GAsyncReadyCallback) sysprof_display_load_cb,
-                                   g_object_ref (self));
-  gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (priv->capture_view));
+  g_task_set_task_data (task, g_memdup (&aids->len, sizeof aids->len), g_free);
+
+  for (guint i = 0; i < aids->len; i++)
+    {
+      SysprofAid *aid = g_ptr_array_index (aids, i);
+
+      sysprof_aid_present_async (aid,
+                                 reader,
+                                 self,
+                                 cancellable,
+                                 sysprof_display_present_cb,
+                                 g_object_ref (task));
+    }
+}
+
+static gboolean
+sysprof_display_present_finish (SysprofDisplay  *self,
+                                GAsyncResult    *result,
+                                GError         **error)
+{
+  g_return_val_if_fail (SYSPROF_IS_DISPLAY (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
 }
 
 static void
-sysprof_display_open_worker (GTask        *task,
+sysprof_display_scan_worker (GTask        *task,
                              gpointer      source_object,
                              gpointer      task_data,
                              GCancellable *cancellable)
 {
-  g_autofree gchar *path = NULL;
+  SysprofDisplay *self = source_object;
+  SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
+  SysprofCaptureReader *reader = task_data;
+  g_autoptr(GHashTable) mark_stats = NULL;
+  g_autoptr(GArray) marks = NULL;
+  SysprofCaptureFrame frame;
+  SysprofCaptureStat st = {{0}};
+  SysprofCaptureFlags flags = 0;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (SYSPROF_IS_DISPLAY (self));
+  g_assert (reader != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /* Scan the reader until the end so that we know we have gotten
+   * all of the timing data loaded into the underlying reader.
+   */
+
+  mark_stats = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+  marks = g_array_new (FALSE, FALSE, sizeof (SysprofMarkStat));
+
+  while (sysprof_capture_reader_peek_frame (reader, &frame))
+    {
+      st.frame_count[frame.type]++;
+
+      if (frame.type == SYSPROF_CAPTURE_FRAME_METADATA)
+        {
+          const SysprofCaptureMetadata *meta;
+
+          if ((meta = sysprof_capture_reader_read_metadata (reader)))
+            {
+              if (g_strcmp0 (meta->id, "local-profiler") == 0)
+                flags |= SYSPROF_CAPTURE_FLAGS_CAN_REPLAY;
+            }
+        }
+      else if (frame.type == SYSPROF_CAPTURE_FRAME_MARK)
+        {
+          const SysprofCaptureMark *mark;
+
+          if ((mark = sysprof_capture_reader_read_mark (reader)))
+            {
+              SysprofMarkStat *mstat;
+              gchar name[152];
+              gpointer idx;
+
+              g_snprintf (name, sizeof name, "%s:%s", mark->group, mark->name);
+
+              if (!(idx = g_hash_table_lookup (mark_stats, name)))
+                {
+                  SysprofMarkStat empty = {{0}};
+
+                  g_strlcpy (empty.name, name, sizeof empty.name);
+                  g_array_append_val (marks, empty);
+                  idx = GUINT_TO_POINTER (marks->len);
+                  g_hash_table_insert (mark_stats, g_strdup (name), idx);
+                }
+
+              mstat = &g_array_index (marks, SysprofMarkStat, GPOINTER_TO_UINT (idx) - 1);
+
+              if (mark->duration > 0)
+                {
+                  if (mstat->min == 0 || mark->duration < mstat->min)
+                    mstat->min = mark->duration;
+                }
+
+              if (mark->duration > mstat->max)
+                mstat->max = mark->duration;
+
+              if (mark->duration > 0)
+                {
+                  mstat->avg += mark->duration;
+                  mstat->avg_count++;
+                }
+
+              mstat->count++;
+            }
+        }
+      else
+        {
+          sysprof_capture_reader_skip (reader);
+        }
+    }
+
+  {
+    GHashTableIter iter;
+    gpointer k,v;
+
+    g_hash_table_iter_init (&iter, mark_stats);
+    while (g_hash_table_iter_next (&iter, &k, &v))
+      {
+        guint idx = GPOINTER_TO_UINT (v) - 1;
+        SysprofMarkStat *mstat = &g_array_index (marks, SysprofMarkStat, idx);
+
+        if (mstat->avg_count > 0 && mstat->avg > 0)
+          mstat->avg /= mstat->avg_count;
+
+#if 0
+        g_print ("%s: count=%ld avg=%ld min=%ld max=%ld\n",
+                 (gchar*)k,
+                 ((SysprofMarkStat *)v)->count,
+                 ((SysprofMarkStat *)v)->avg,
+                 ((SysprofMarkStat *)v)->min,
+                 ((SysprofMarkStat *)v)->max);
+#endif
+      }
+  }
+
+  g_object_set_data_full (G_OBJECT (task),
+                          "MARK_STAT",
+                          g_steal_pointer (&marks),
+                          (GDestroyNotify) g_array_unref);
+
+  g_atomic_int_set (&priv->flags, flags);
+  sysprof_capture_reader_reset (reader);
+  sysprof_capture_reader_set_stat (reader, &st);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+sysprof_display_scan_async (SysprofDisplay       *self,
+                            SysprofCaptureReader *reader,
+                            GCancellable         *cancellable,
+                            GAsyncReadyCallback   callback,
+                            gpointer              user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (SYSPROF_IS_DISPLAY (self));
+  g_return_if_fail (reader != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_display_scan_async);
+  g_task_set_task_data (task,
+                        sysprof_capture_reader_ref (reader),
+                        (GDestroyNotify) sysprof_capture_reader_unref);
+  g_task_run_in_thread (task, sysprof_display_scan_worker);
+}
+
+static gboolean
+sysprof_display_scan_finish (SysprofDisplay  *self,
+                             GAsyncResult    *result,
+                             GError         **error)
+{
+  SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
+  GArray *marks;
+
+  g_return_val_if_fail (SYSPROF_IS_DISPLAY (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  if ((marks = g_object_get_data (G_OBJECT (result), "MARK_STAT")))
+    sysprof_details_page_add_marks (priv->details,
+                                    (const SysprofMarkStat *)(gpointer)marks->data,
+                                    marks->len);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+sysprof_display_load_present_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  SysprofDisplay *self = (SysprofDisplay *)object;
   g_autoptr(GError) error = NULL;
+  g_autoptr(GTask) task = user_data;
+
+  g_assert (SYSPROF_IS_DISPLAY (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!sysprof_display_present_finish (self, result, &error))
+    g_warning ("Error presenting: %s", error->message);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+sysprof_display_load_frame_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  SysprofVisualizersFrame *frame = (SysprofVisualizersFrame *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GTask) task = user_data;
   SysprofCaptureReader *reader;
-  GFile *file = task_data;
+  SysprofDisplay *self;
+  GCancellable *cancellable;
 
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (frame));
+  g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (G_IS_TASK (task));
-  g_assert (source_object == NULL);
-  g_assert (G_IS_FILE (file));
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  path = g_file_get_path (file);
+  self = g_task_get_source_object (task);
+  reader = g_task_get_task_data (task);
+  cancellable = g_task_get_cancellable (task);
 
-  if (!(reader = sysprof_capture_reader_new (path, &error)))
-    g_task_return_error (task, g_steal_pointer ((&error)));
+  if (!sysprof_visualizers_frame_load_finish (frame, result, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
   else
-    g_task_return_pointer (task,
-                           g_steal_pointer (&reader),
-                           (GDestroyNotify) sysprof_capture_reader_unref);
+    sysprof_display_present_async (self,
+                                   reader,
+                                   cancellable,
+                                   sysprof_display_load_present_cb,
+                                   g_steal_pointer (&task));
 }
 
-void
-sysprof_display_open (SysprofDisplay *self,
-                      GFile          *file)
+static void
+sysprof_display_load_scan_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
 {
+  SysprofDisplay *self = (SysprofDisplay *)object;
   SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
-  g_autoptr(GTask) task = NULL;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  SysprofCaptureReader *reader;
+  SysprofSelection *selection;
+  GCancellable *cancellable;
+  GList *pages;
 
-  g_return_if_fail (SYSPROF_IS_DISPLAY (self));
-  g_return_if_fail (G_IS_FILE (file));
-  g_return_if_fail (g_file_is_native (file));
-  g_return_if_fail (sysprof_display_is_empty (self));
+  g_assert (SYSPROF_IS_DISPLAY (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
 
-  g_set_object (&priv->file, file);
+  reader = g_task_get_task_data (task);
+  cancellable = g_task_get_cancellable (task);
 
-  task = g_task_new (NULL, NULL, sysprof_display_open_cb, g_object_ref (self));
-  g_task_set_task_data (task, g_file_dup (file), g_object_unref);
-  g_task_run_in_thread (task, sysprof_display_open_worker);
+  if (!sysprof_display_scan_finish (self, result, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    sysprof_visualizers_frame_load_async (priv->visualizers,
+                                          reader,
+                                          cancellable,
+                                          sysprof_display_load_frame_cb,
+                                          g_steal_pointer (&task));
 
-  update_title_child_property (self);
+  selection = sysprof_visualizers_frame_get_selection (priv->visualizers);
+
+  sysprof_details_page_set_reader (priv->details, reader);
+
+  /* Opportunistically load pages */
+  pages = gtk_container_get_children (GTK_CONTAINER (priv->pages));
+  for (const GList *iter = pages; iter; iter = iter->next)
+    {
+      if (SYSPROF_IS_PAGE (iter->data))
+        sysprof_page_load_async (iter->data,
+                                 reader,
+                                 selection,
+                                 priv->filter,
+                                 NULL, NULL, NULL);
+    }
+  g_list_free (pages);
+
+  gtk_stack_set_visible_child_name (priv->stack, "view");
 }
 
 void
-sysprof_display_save (SysprofDisplay *self)
+sysprof_display_load_async (SysprofDisplay       *self,
+                            SysprofCaptureReader *reader,
+                            GCancellable         *cancellable,
+                            GAsyncReadyCallback   callback,
+                            gpointer              user_data)
 {
   SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
-  g_autoptr(GFile) file = NULL;
-  GtkFileChooserNative *native;
-  SysprofCaptureReader *reader;
-  GtkWindow *parent;
-  gint res;
+  g_autoptr(GTask) task = NULL;
 
   g_return_if_fail (SYSPROF_IS_DISPLAY (self));
+  g_return_if_fail (reader != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  if (!(reader = sysprof_capture_view_get_reader (priv->capture_view)))
-    return;
+  if (priv->reader != reader)
+    {
+      g_clear_pointer (&priv->reader, sysprof_capture_reader_unref);
+      priv->reader = sysprof_capture_reader_ref (reader);
+    }
 
-  parent = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self)));
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_display_load_async);
+  g_task_set_task_data (task,
+                        sysprof_capture_reader_ref (reader),
+                        (GDestroyNotify) sysprof_capture_reader_unref);
+
+  /* First scan the reader for any sort of data we care about before
+   * we notify aids to load content. That allows us to ensure we have
+   * proper timing data for the consumers.
+   */
+  sysprof_display_scan_async (self,
+                              reader,
+                              cancellable,
+                              sysprof_display_load_scan_cb,
+                              g_steal_pointer (&task));
+}
 
-  native = gtk_file_chooser_native_new (_("Save Recording"),
-                                        parent,
-                                        GTK_FILE_CHOOSER_ACTION_SAVE,
-                                        _("Save"),
-                                        _("Cancel"));
-  gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (native), TRUE);
-  gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (native), TRUE);
-  gtk_file_chooser_set_create_folders (GTK_FILE_CHOOSER (native), TRUE);
-  gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (native), "capture.syscap");
+gboolean
+sysprof_display_load_finish (SysprofDisplay  *self,
+                             GAsyncResult    *result,
+                             GError         **error)
+{
+  g_return_val_if_fail (SYSPROF_IS_DISPLAY (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
 
-  res = gtk_native_dialog_run (GTK_NATIVE_DIALOG (native));
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
 
-  switch (res)
-    {
-    case GTK_RESPONSE_ACCEPT:
-      file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (native));
+SysprofZoomManager *
+sysprof_display_get_zoom_manager (SysprofDisplay *self)
+{
+  SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
 
-      if (g_file_is_native (file))
-        {
-          g_autofree gchar *path = g_file_get_path (file);
-          g_autoptr(GError) error = NULL;
+  g_return_val_if_fail (SYSPROF_IS_DISPLAY (self), NULL);
 
-          if (!sysprof_capture_reader_save_as (reader, path, &error))
-            {
-              GtkWidget *msg;
+  return sysprof_visualizers_frame_get_zoom_manager (priv->visualizers);
+}
 
-              msg = gtk_message_dialog_new (parent,
-                                            GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT | 
GTK_DIALOG_USE_HEADER_BAR,
-                                            GTK_MESSAGE_ERROR,
-                                            GTK_BUTTONS_CLOSE,
-                                            _("Failed to save recording: %s"),
-                                            error->message);
-              gtk_window_present (GTK_WINDOW (msg));
-              g_signal_connect (msg, "response", G_CALLBACK (gtk_widget_destroy), NULL);
-            }
-        }
+/**
+ * sysprof_display_is_empty:
+ *
+ * Checks if any content is or will be loaded into @self.
+ *
+ * Returns: %TRUE if the tab is unperterbed.
+ *
+ * Since: 3.34
+ */
+gboolean
+sysprof_display_is_empty (SysprofDisplay *self)
+{
+  SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
 
-      break;
+  g_return_val_if_fail (SYSPROF_IS_DISPLAY (self), FALSE);
 
-    default:
-      break;
-    }
+  return priv->file == NULL &&
+         priv->profiler == NULL &&
+         gtk_stack_get_visible_child (priv->stack) == GTK_WIDGET (priv->assistant) &&
+         NULL == priv->reader;
+}
 
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
-  gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (native));
+void
+sysprof_display_open (SysprofDisplay *self,
+                      GFile          *file)
+{
+  SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
+  g_autoptr(SysprofCaptureReader) reader = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GTask) task = NULL;
+  g_autofree gchar *path = NULL;
+
+  g_return_if_fail (SYSPROF_IS_DISPLAY (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (g_file_is_native (file));
+  g_return_if_fail (sysprof_display_is_empty (self));
+
+  g_set_object (&priv->file, file);
+
+  path = g_file_get_path (file);
+
+  if (!(reader = sysprof_capture_reader_new (path, &error)))
+    g_warning ("Failed to open capture: %s", error->message);
+  else
+    sysprof_display_load_async (self, reader, NULL, NULL, NULL);
+
+  update_title_child_property (self);
 }
 
 gboolean
@@ -593,7 +1046,7 @@ sysprof_display_get_can_save (SysprofDisplay *self)
 
   g_return_val_if_fail (SYSPROF_IS_DISPLAY (self), FALSE);
 
-  return sysprof_capture_view_get_reader (priv->capture_view) != NULL;
+  return priv->reader != NULL;
 }
 
 void
@@ -625,7 +1078,8 @@ sysprof_display_get_can_replay (SysprofDisplay *self)
   g_return_val_if_fail (SYSPROF_IS_DISPLAY (self), FALSE);
 
   return !sysprof_display_is_empty (self) &&
-          sysprof_capture_view_get_can_replay (priv->capture_view);
+          priv->reader != NULL &&
+          !!(priv->flags & SYSPROF_CAPTURE_FLAGS_CAN_REPLAY);
 }
 
 /**
@@ -645,15 +1099,12 @@ sysprof_display_replay (SysprofDisplay *self)
 {
   SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
   g_autoptr(SysprofProfiler) profiler = NULL;
-  SysprofCaptureReader *reader;
   SysprofDisplay *copy;
 
   g_return_val_if_fail (SYSPROF_IS_DISPLAY (self), NULL);
+  g_return_val_if_fail (priv->reader != NULL, NULL);
 
-  reader = sysprof_capture_view_get_reader (priv->capture_view);
-  g_return_val_if_fail (reader != NULL, NULL);
-
-  profiler = sysprof_local_profiler_new_replay (reader);
+  profiler = sysprof_local_profiler_new_replay (priv->reader);
   g_return_val_if_fail (profiler != NULL, NULL);
   g_return_val_if_fail (SYSPROF_IS_LOCAL_PROFILER (profiler), NULL);
 
@@ -676,3 +1127,64 @@ sysprof_display_new_for_profiler (SysprofProfiler *profiler)
 
   return GTK_WIDGET (g_steal_pointer (&self));
 }
+
+void
+sysprof_display_save (SysprofDisplay *self)
+{
+  SysprofDisplayPrivate *priv = sysprof_display_get_instance_private (self);
+  g_autoptr(GFile) file = NULL;
+  GtkFileChooserNative *native;
+  GtkWindow *parent;
+  gint res;
+
+  g_return_if_fail (SYSPROF_IS_DISPLAY (self));
+  g_return_if_fail (priv->reader != NULL);
+
+  parent = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self)));
+
+  native = gtk_file_chooser_native_new (_("Save Recording"),
+                                        parent,
+                                        GTK_FILE_CHOOSER_ACTION_SAVE,
+                                        _("Save"),
+                                        _("Cancel"));
+  gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (native), TRUE);
+  gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (native), TRUE);
+  gtk_file_chooser_set_create_folders (GTK_FILE_CHOOSER (native), TRUE);
+  gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (native), "capture.syscap");
+
+  res = gtk_native_dialog_run (GTK_NATIVE_DIALOG (native));
+
+  switch (res)
+    {
+    case GTK_RESPONSE_ACCEPT:
+      file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (native));
+
+      if (g_file_is_native (file))
+        {
+          g_autofree gchar *path = g_file_get_path (file);
+          g_autoptr(GError) error = NULL;
+
+          if (!sysprof_capture_reader_save_as (priv->reader, path, &error))
+            {
+              GtkWidget *msg;
+
+              msg = gtk_message_dialog_new (parent,
+                                            GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT | 
GTK_DIALOG_USE_HEADER_BAR,
+                                            GTK_MESSAGE_ERROR,
+                                            GTK_BUTTONS_CLOSE,
+                                            _("Failed to save recording: %s"),
+                                            error->message);
+              gtk_window_present (GTK_WINDOW (msg));
+              g_signal_connect (msg, "response", G_CALLBACK (gtk_widget_destroy), NULL);
+            }
+        }
+
+      break;
+
+    default:
+      break;
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+  gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (native));
+}
diff --git a/src/libsysprof-ui/sysprof-display.h b/src/libsysprof-ui/sysprof-display.h
index 96d780b..7f0ea9c 100644
--- a/src/libsysprof-ui/sysprof-display.h
+++ b/src/libsysprof-ui/sysprof-display.h
@@ -23,7 +23,9 @@
 #include <gtk/gtk.h>
 #include <sysprof.h>
 
-#include "sysprof-version-macros.h"
+#include "sysprof-page.h"
+#include "sysprof-visualizer-group.h"
+#include "sysprof-zoom-manager.h"
 
 G_BEGIN_DECLS
 
@@ -32,38 +34,59 @@ G_BEGIN_DECLS
 SYSPROF_AVAILABLE_IN_ALL
 G_DECLARE_DERIVABLE_TYPE (SysprofDisplay, sysprof_display, SYSPROF, DISPLAY, GtkBin)
 
-SYSPROF_ALIGNED_BEGIN(8)
 struct _SysprofDisplayClass
 {
   GtkBinClass parent_class;
 
   /*< private >*/
   gpointer _reserved[16];
-}
-SYSPROF_ALIGNED_END(8);
+};
 
 SYSPROF_AVAILABLE_IN_ALL
-GtkWidget       *sysprof_display_new              (void);
+GtkWidget          *sysprof_display_new              (void);
 SYSPROF_AVAILABLE_IN_ALL
-GtkWidget       *sysprof_display_new_for_profiler (SysprofProfiler *profiler);
+GtkWidget          *sysprof_display_new_for_profiler (SysprofProfiler         *profiler);
 SYSPROF_AVAILABLE_IN_ALL
-gchar           *sysprof_display_dup_title        (SysprofDisplay  *self);
+char               *sysprof_display_dup_title        (SysprofDisplay          *self);
 SYSPROF_AVAILABLE_IN_ALL
-SysprofProfiler *sysprof_display_get_profiler     (SysprofDisplay  *self);
+SysprofProfiler    *sysprof_display_get_profiler     (SysprofDisplay          *self);
 SYSPROF_AVAILABLE_IN_ALL
-gboolean         sysprof_display_is_empty         (SysprofDisplay  *self);
+void                sysprof_display_add_group        (SysprofDisplay          *self,
+                                                      SysprofVisualizerGroup  *group);
 SYSPROF_AVAILABLE_IN_ALL
-void             sysprof_display_open             (SysprofDisplay  *self,
-                                                   GFile           *file);
+void                sysprof_display_add_page         (SysprofDisplay          *self,
+                                                      SysprofPage             *page);
 SYSPROF_AVAILABLE_IN_ALL
-void             sysprof_display_save             (SysprofDisplay  *self);
+SysprofPage        *sysprof_display_get_visible_page (SysprofDisplay          *self);
 SYSPROF_AVAILABLE_IN_ALL
-gboolean         sysprof_display_get_can_save     (SysprofDisplay  *self);
+void                sysprof_display_set_visible_page (SysprofDisplay          *self,
+                                                      SysprofPage             *page);
 SYSPROF_AVAILABLE_IN_ALL
-void             sysprof_display_stop_recording   (SysprofDisplay  *self);
+SysprofZoomManager *sysprof_display_get_zoom_manager (SysprofDisplay          *self);
 SYSPROF_AVAILABLE_IN_ALL
-gboolean         sysprof_display_get_can_replay   (SysprofDisplay  *self);
+void                sysprof_display_load_async       (SysprofDisplay          *self,
+                                                      SysprofCaptureReader    *reader,
+                                                      GCancellable            *cancellable,
+                                                      GAsyncReadyCallback      callback,
+                                                      gpointer                 user_data);
 SYSPROF_AVAILABLE_IN_ALL
-SysprofDisplay  *sysprof_display_replay           (SysprofDisplay  *self);
+gboolean            sysprof_display_load_finish      (SysprofDisplay          *self,
+                                                      GAsyncResult            *result,
+                                                      GError                 **error);
+SYSPROF_AVAILABLE_IN_ALL
+gboolean            sysprof_display_is_empty         (SysprofDisplay          *self);
+SYSPROF_AVAILABLE_IN_ALL
+void                sysprof_display_open             (SysprofDisplay          *self,
+                                                      GFile                   *file);
+SYSPROF_AVAILABLE_IN_ALL
+void                sysprof_display_save             (SysprofDisplay          *self);
+SYSPROF_AVAILABLE_IN_ALL
+gboolean            sysprof_display_get_can_save     (SysprofDisplay          *self);
+SYSPROF_AVAILABLE_IN_ALL
+void                sysprof_display_stop_recording   (SysprofDisplay          *self);
+SYSPROF_AVAILABLE_IN_ALL
+gboolean            sysprof_display_get_can_replay   (SysprofDisplay          *self);
+SYSPROF_AVAILABLE_IN_ALL
+SysprofDisplay     *sysprof_display_replay           (SysprofDisplay          *self);
 
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-display.ui b/src/libsysprof-ui/sysprof-display.ui
index a9af096..43661e6 100644
--- a/src/libsysprof-ui/sysprof-display.ui
+++ b/src/libsysprof-ui/sysprof-display.ui
@@ -14,11 +14,40 @@
           </packing>
         </child>
         <child>
-          <object class="SysprofCaptureView" id="capture_view">
+          <object class="DzlMultiPaned">
+            <property name="orientation">vertical</property>
             <property name="visible">true</property>
+            <child>
+              <object class="SysprofVisualizersFrame" id="visualizers">
+                <property name="vexpand">false</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkSeparator">
+                <property name="orientation">horizontal</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkStack" id="pages">
+                <property name="homogeneous">false</property>
+                <property name="vexpand">true</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="SysprofDetailsPage" id="details">
+                    <property name="visible">true</property>
+                  </object>
+                  <packing>
+                    <property name="title" translatable="yes">Details</property>
+                    <property name="name">details</property>
+                  </packing>
+                </child>
+              </object>
+            </child>
           </object>
           <packing>
-            <property name="name">capture</property>
+            <property name="name">view</property>
           </packing>
         </child>
         <child>
@@ -26,14 +55,11 @@
             <property name="visible">true</property>
           </object>
           <packing>
-            <property name="name">recording</property>
+            <property name="name">record</property>
           </packing>
         </child>
         <child>
-          <object class="SysprofEmptyStateView" id="failed_view">
-            <property name="icon-name">computer-fail-symbolic</property>
-            <property name="title" translatable="yes">Something went wrong</property>
-            <property name="subtitle" translatable="yes">Sysprof failed to access the requested performance 
data.</property>
+          <object class="SysprofFailedStateView" id="failed_view">
             <property name="visible">true</property>
           </object>
           <packing>
diff --git a/src/libsysprof-ui/sysprof-empty-state-view.h b/src/libsysprof-ui/sysprof-empty-state-view.h
index 9e7aa72..4c15aec 100644
--- a/src/libsysprof-ui/sysprof-empty-state-view.h
+++ b/src/libsysprof-ui/sysprof-empty-state-view.h
@@ -20,10 +20,6 @@
 
 #pragma once
 
-#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
-# error "Only <sysprof-ui.h> can be included directly."
-#endif
-
 #include <gtk/gtk.h>
 #include <sysprof.h>
 
@@ -31,7 +27,6 @@ G_BEGIN_DECLS
 
 #define SYSPROF_TYPE_EMPTY_STATE_VIEW (sysprof_empty_state_view_get_type())
 
-SYSPROF_AVAILABLE_IN_ALL
 G_DECLARE_DERIVABLE_TYPE (SysprofEmptyStateView, sysprof_empty_state_view, SYSPROF, EMPTY_STATE_VIEW, GtkBin)
 
 struct _SysprofEmptyStateViewClass
@@ -41,7 +36,6 @@ struct _SysprofEmptyStateViewClass
   gpointer padding[4];
 };
 
-SYSPROF_AVAILABLE_IN_ALL
 GtkWidget *sysprof_empty_state_view_new (void);
 
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-failed-state-view.h b/src/libsysprof-ui/sysprof-failed-state-view.h
index ad14efc..91ed603 100644
--- a/src/libsysprof-ui/sysprof-failed-state-view.h
+++ b/src/libsysprof-ui/sysprof-failed-state-view.h
@@ -20,10 +20,6 @@
 
 #pragma once
 
-#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
-# error "Only <sysprof-ui.h> can be included directly."
-#endif
-
 #include <gtk/gtk.h>
 #include <sysprof.h>
 
@@ -31,7 +27,6 @@ G_BEGIN_DECLS
 
 #define SYSPROF_TYPE_FAILED_STATE_VIEW (sysprof_failed_state_view_get_type())
 
-SYSPROF_AVAILABLE_IN_ALL
 G_DECLARE_DERIVABLE_TYPE (SysprofFailedStateView, sysprof_failed_state_view, SYSPROF, FAILED_STATE_VIEW, 
GtkBin)
 
 struct _SysprofFailedStateViewClass
@@ -41,9 +36,7 @@ struct _SysprofFailedStateViewClass
   gpointer padding[4];
 };
 
-SYSPROF_AVAILABLE_IN_ALL
 GtkWidget *sysprof_failed_state_view_new          (void);
-SYSPROF_AVAILABLE_IN_ALL
 void       sysprof_failed_state_view_set_profiler (SysprofFailedStateView *self,
                                                    SysprofProfiler        *profiler);
 
diff --git a/src/libsysprof-ui/sysprof-line-visualizer-row.c b/src/libsysprof-ui/sysprof-line-visualizer.c
similarity index 62%
rename from src/libsysprof-ui/sysprof-line-visualizer-row.c
rename to src/libsysprof-ui/sysprof-line-visualizer.c
index 670f84f..1b0295e 100644
--- a/src/libsysprof-ui/sysprof-line-visualizer-row.c
+++ b/src/libsysprof-ui/sysprof-line-visualizer.c
@@ -1,4 +1,4 @@
-/* sysprof-line-visualizer-row.c
+/* sysprof-line-visualizer.c
  *
  * Copyright 2016-2019 Christian Hergert <christian hergert me>
  *
@@ -18,7 +18,7 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#define G_LOG_DOMAIN "sysprof-line-visualizer-row"
+#define G_LOG_DOMAIN "sysprof-line-visualizer"
 
 #include "config.h"
 
@@ -27,7 +27,7 @@
 #include <sysprof.h>
 
 #include "pointcache.h"
-#include "sysprof-line-visualizer-row.h"
+#include "sysprof-line-visualizer.h"
 
 typedef struct
 {
@@ -49,11 +49,6 @@ typedef struct
    */
   PointCache *cache;
 
-  /*
-   * Child widget to display the label in the upper corner.
-   */
-  GtkLabel *label;
-
   /*
    * Range of the scale for lower and upper.
    */
@@ -69,7 +64,7 @@ typedef struct
 
   guint y_lower_set : 1;
   guint y_upper_set : 1;
-} SysprofLineVisualizerRowPrivate;
+} SysprofLineVisualizerPrivate;
 
 typedef struct
 {
@@ -96,19 +91,18 @@ typedef struct
   guint y_upper_set : 1;
 } LoadData;
 
-G_DEFINE_TYPE_WITH_PRIVATE (SysprofLineVisualizerRow, sysprof_line_visualizer_row, 
SYSPROF_TYPE_VISUALIZER_ROW)
+G_DEFINE_TYPE_WITH_PRIVATE (SysprofLineVisualizer, sysprof_line_visualizer, SYSPROF_TYPE_VISUALIZER)
 
-static void       sysprof_line_visualizer_row_load_data_async   (SysprofLineVisualizerRow  *self,
-                                                                 GCancellable         *cancellable,
-                                                                 GAsyncReadyCallback   callback,
-                                                                 gpointer              user_data);
-static PointCache *sysprof_line_visualizer_row_load_data_finish (SysprofLineVisualizerRow  *self,
-                                                                 GAsyncResult         *result,
-                                                                 GError              **error);
+static void        sysprof_line_visualizer_load_data_async  (SysprofLineVisualizer  *self,
+                                                             GCancellable           *cancellable,
+                                                             GAsyncReadyCallback     callback,
+                                                             gpointer                user_data);
+static PointCache *sysprof_line_visualizer_load_data_finish (SysprofLineVisualizer  *self,
+                                                             GAsyncResult           *result,
+                                                             GError                **error);
 
 enum {
   PROP_0,
-  PROP_TITLE,
   PROP_Y_LOWER,
   PROP_Y_UPPER,
   N_PROPS
@@ -144,34 +138,38 @@ copy_array (GArray *ar)
 }
 
 static gboolean
-sysprof_line_visualizer_row_draw (GtkWidget *widget,
-                                  cairo_t   *cr)
+sysprof_line_visualizer_draw (GtkWidget *widget,
+                              cairo_t   *cr)
 {
-  SysprofLineVisualizerRow *self = (SysprofLineVisualizerRow *)widget;
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizer *self = (SysprofLineVisualizer *)widget;
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
   GtkStyleContext *style_context;
   GtkStateFlags flags;
   GtkAllocation alloc;
+  GdkRectangle clip;
   GdkRGBA foreground;
   gboolean ret;
 
-  g_assert (SYSPROF_IS_LINE_VISUALIZER_ROW (widget));
+  g_assert (SYSPROF_IS_LINE_VISUALIZER (widget));
   g_assert (cr != NULL);
 
   gtk_widget_get_allocation (widget, &alloc);
 
-  ret = GTK_WIDGET_CLASS (sysprof_line_visualizer_row_parent_class)->draw (widget, cr);
+  ret = GTK_WIDGET_CLASS (sysprof_line_visualizer_parent_class)->draw (widget, cr);
 
   if (priv->cache == NULL)
     return ret;
 
+  if (!gdk_cairo_get_clip_rectangle (cr, &clip))
+    return ret;
+
   style_context = gtk_widget_get_style_context (widget);
   flags = gtk_widget_get_state_flags (widget);
   gtk_style_context_get_color (style_context, flags, &foreground);
 
   for (guint line = 0; line < priv->lines->len; line++)
     {
-      g_autofree SysprofVisualizerRowAbsolutePoint *points = NULL;
+      g_autofree SysprofVisualizerAbsolutePoint *points = NULL;
       const LineInfo *line_info = &g_array_index (priv->lines, LineInfo, line);
       const Point *fpoints;
       guint n_fpoints = 0;
@@ -181,19 +179,32 @@ sysprof_line_visualizer_row_draw (GtkWidget *widget,
 
       if (n_fpoints > 0)
         {
-          gdouble last_x;
-          gdouble last_y;
+          gdouble last_x = 0;
+          gdouble last_y = 0;
+          guint p;
 
-          points = g_new0 (SysprofVisualizerRowAbsolutePoint, n_fpoints);
+          points = g_new0 (SysprofVisualizerAbsolutePoint, n_fpoints);
 
-          sysprof_visualizer_row_translate_points (SYSPROF_VISUALIZER_ROW (self),
-                                                   (const SysprofVisualizerRowRelativePoint *)fpoints,
-                                                   n_fpoints,
-                                                   points,
-                                                   n_fpoints);
+          sysprof_visualizer_translate_points (SYSPROF_VISUALIZER (self),
+                                               (const SysprofVisualizerRelativePoint *)fpoints,
+                                               n_fpoints,
+                                               points,
+                                               n_fpoints);
 
-          last_x = points[0].x;
-          last_y = points[0].y;
+          for (p = 0; p < n_fpoints; p++)
+            {
+              if (points[p].x >= clip.x)
+                break;
+            }
+
+          if (p >= n_fpoints)
+            return ret;
+
+          if (p > 0)
+            p--;
+
+          last_x = points[p].x;
+          last_y = points[p].y;
 
           if (line_info->fill)
             {
@@ -205,7 +216,7 @@ sysprof_line_visualizer_row_draw (GtkWidget *widget,
               cairo_move_to (cr, last_x, last_y);
             }
 
-          for (guint i = 1; i < n_fpoints; i++)
+          for (guint i = p + 1; i < n_fpoints; i++)
             {
               cairo_curve_to (cr,
                               last_x + ((points[i].x - last_x) / 2),
@@ -214,8 +225,12 @@ sysprof_line_visualizer_row_draw (GtkWidget *widget,
                               points[i].y,
                               points[i].x,
                               points[i].y);
+
               last_x = points[i].x;
               last_y = points[i].y;
+
+              if (points[i].x > clip.x + clip.width)
+                break;
             }
 
           if (line_info->fill)
@@ -249,18 +264,18 @@ sysprof_line_visualizer_row_draw (GtkWidget *widget,
 }
 
 static void
-sysprof_line_visualizer_row_load_data_cb (GObject      *object,
+sysprof_line_visualizer_load_data_cb (GObject      *object,
                                           GAsyncResult *result,
                                           gpointer      user_data)
 {
-  SysprofLineVisualizerRow *self = (SysprofLineVisualizerRow *)object;
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizer *self = (SysprofLineVisualizer *)object;
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
   g_autoptr(GError) error = NULL;
   g_autoptr(PointCache) cache = NULL;
 
-  g_assert (SYSPROF_IS_LINE_VISUALIZER_ROW (self));
+  g_assert (SYSPROF_IS_LINE_VISUALIZER (self));
 
-  cache = sysprof_line_visualizer_row_load_data_finish (self, result, &error);
+  cache = sysprof_line_visualizer_load_data_finish (self, result, &error);
 
   if (cache == NULL)
     {
@@ -275,20 +290,20 @@ sysprof_line_visualizer_row_load_data_cb (GObject      *object,
 }
 
 static gboolean
-sysprof_line_visualizer_row_do_reload (gpointer data)
+sysprof_line_visualizer_do_reload (gpointer data)
 {
-  SysprofLineVisualizerRow *self = data;
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizer *self = data;
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
 
-  g_assert (SYSPROF_IS_LINE_VISUALIZER_ROW (self));
+  g_assert (SYSPROF_IS_LINE_VISUALIZER (self));
 
   priv->queued_load = 0;
 
   if (priv->reader != NULL)
     {
-      sysprof_line_visualizer_row_load_data_async (self,
+      sysprof_line_visualizer_load_data_async (self,
                                               NULL,
-                                              sysprof_line_visualizer_row_load_data_cb,
+                                              sysprof_line_visualizer_load_data_cb,
                                               NULL);
     }
 
@@ -296,29 +311,29 @@ sysprof_line_visualizer_row_do_reload (gpointer data)
 }
 
 static void
-sysprof_line_visualizer_row_queue_reload (SysprofLineVisualizerRow *self)
+sysprof_line_visualizer_queue_reload (SysprofLineVisualizer *self)
 {
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
 
-  g_assert (SYSPROF_IS_LINE_VISUALIZER_ROW (self));
+  g_assert (SYSPROF_IS_LINE_VISUALIZER (self));
 
   if (priv->queued_load == 0)
     {
       priv->queued_load = gdk_threads_add_idle_full (G_PRIORITY_LOW,
-                                                     sysprof_line_visualizer_row_do_reload,
+                                                     sysprof_line_visualizer_do_reload,
                                                      self,
                                                      NULL);
     }
 }
 
 static void
-sysprof_line_visualizer_row_set_reader (SysprofVisualizerRow *row,
-                                        SysprofCaptureReader *reader)
+sysprof_line_visualizer_set_reader (SysprofVisualizer    *row,
+                                    SysprofCaptureReader *reader)
 {
-  SysprofLineVisualizerRow *self = (SysprofLineVisualizerRow *)row;
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizer *self = (SysprofLineVisualizer *)row;
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
 
-  g_assert (SYSPROF_IS_LINE_VISUALIZER_ROW (self));
+  g_assert (SYSPROF_IS_LINE_VISUALIZER (self));
 
   if (priv->reader != reader)
     {
@@ -331,15 +346,15 @@ sysprof_line_visualizer_row_set_reader (SysprofVisualizerRow *row,
       if (reader != NULL)
         priv->reader = sysprof_capture_reader_ref (reader);
 
-      sysprof_line_visualizer_row_queue_reload (self);
+      sysprof_line_visualizer_queue_reload (self);
     }
 }
 
 static void
-sysprof_line_visualizer_row_finalize (GObject *object)
+sysprof_line_visualizer_finalize (GObject *object)
 {
-  SysprofLineVisualizerRow *self = (SysprofLineVisualizerRow *)object;
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizer *self = (SysprofLineVisualizer *)object;
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
 
   g_clear_pointer (&priv->lines, g_array_unref);
   g_clear_pointer (&priv->cache, point_cache_unref);
@@ -351,24 +366,20 @@ sysprof_line_visualizer_row_finalize (GObject *object)
       priv->queued_load = 0;
     }
 
-  G_OBJECT_CLASS (sysprof_line_visualizer_row_parent_class)->finalize (object);
+  G_OBJECT_CLASS (sysprof_line_visualizer_parent_class)->finalize (object);
 }
 
 static void
-sysprof_line_visualizer_row_get_property (GObject    *object,
-                                          guint       prop_id,
-                                          GValue     *value,
-                                          GParamSpec *pspec)
+sysprof_line_visualizer_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
 {
-  SysprofLineVisualizerRow *self = SYSPROF_LINE_VISUALIZER_ROW (object);
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizer *self = SYSPROF_LINE_VISUALIZER (object);
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
 
   switch (prop_id)
     {
-    case PROP_TITLE:
-      g_object_get_property (G_OBJECT (priv->label), "label", value);
-      break;
-
     case PROP_Y_LOWER:
       g_value_set_double (value, priv->y_lower);
       break;
@@ -383,20 +394,16 @@ sysprof_line_visualizer_row_get_property (GObject    *object,
 }
 
 static void
-sysprof_line_visualizer_row_set_property (GObject      *object,
-                                          guint         prop_id,
-                                          const GValue *value,
-                                          GParamSpec   *pspec)
+sysprof_line_visualizer_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
 {
-  SysprofLineVisualizerRow *self = SYSPROF_LINE_VISUALIZER_ROW (object);
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizer *self = SYSPROF_LINE_VISUALIZER (object);
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
 
   switch (prop_id)
     {
-    case PROP_TITLE:
-      g_object_set_property (G_OBJECT (priv->label), "label", value);
-      break;
-
     case PROP_Y_LOWER:
       priv->y_lower = g_value_get_double (value);
       priv->y_lower_set = TRUE;
@@ -415,26 +422,19 @@ sysprof_line_visualizer_row_set_property (GObject      *object,
 }
 
 static void
-sysprof_line_visualizer_row_class_init (SysprofLineVisualizerRowClass *klass)
+sysprof_line_visualizer_class_init (SysprofLineVisualizerClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
-  SysprofVisualizerRowClass *visualizer_class = SYSPROF_VISUALIZER_ROW_CLASS (klass);
+  SysprofVisualizerClass *visualizer_class = SYSPROF_VISUALIZER_CLASS (klass);
 
-  object_class->finalize = sysprof_line_visualizer_row_finalize;
-  object_class->get_property = sysprof_line_visualizer_row_get_property;
-  object_class->set_property = sysprof_line_visualizer_row_set_property;
+  object_class->finalize = sysprof_line_visualizer_finalize;
+  object_class->get_property = sysprof_line_visualizer_get_property;
+  object_class->set_property = sysprof_line_visualizer_set_property;
 
-  widget_class->draw = sysprof_line_visualizer_row_draw;
+  widget_class->draw = sysprof_line_visualizer_draw;
 
-  visualizer_class->set_reader = sysprof_line_visualizer_row_set_reader;
-
-  properties [PROP_TITLE] =
-    g_param_spec_string ("title",
-                         "Title",
-                         "The title of the row",
-                         NULL,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+  visualizer_class->set_reader = sysprof_line_visualizer_set_reader;
 
   properties [PROP_Y_LOWER] =
     g_param_spec_double ("y-lower",
@@ -458,35 +458,22 @@ sysprof_line_visualizer_row_class_init (SysprofLineVisualizerRowClass *klass)
 }
 
 static void
-sysprof_line_visualizer_row_init (SysprofLineVisualizerRow *self)
+sysprof_line_visualizer_init (SysprofLineVisualizer *self)
 {
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
-  PangoAttrList *attrs = pango_attr_list_new ();
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
 
   priv->lines = g_array_new (FALSE, FALSE, sizeof (LineInfo));
-
-  pango_attr_list_insert (attrs, pango_attr_scale_new (PANGO_SCALE_SMALL * PANGO_SCALE_SMALL));
-
-  priv->label = g_object_new (GTK_TYPE_LABEL,
-                              "attributes", attrs,
-                              "visible", TRUE,
-                              "xalign", 0.0f,
-                              "yalign", 0.0f,
-                              NULL);
-  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (priv->label));
-
-  pango_attr_list_unref (attrs);
 }
 
 void
-sysprof_line_visualizer_row_add_counter (SysprofLineVisualizerRow *self,
-                                         guint                     counter_id,
-                                         const GdkRGBA            *color)
+sysprof_line_visualizer_add_counter (SysprofLineVisualizer *self,
+                                     guint                     counter_id,
+                                     const GdkRGBA            *color)
 {
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
   LineInfo line_info = { 0 };
 
-  g_assert (SYSPROF_IS_LINE_VISUALIZER_ROW (self));
+  g_assert (SYSPROF_IS_LINE_VISUALIZER (self));
   g_assert (priv->lines != NULL);
 
   line_info.id = counter_id;
@@ -506,18 +493,18 @@ sysprof_line_visualizer_row_add_counter (SysprofLineVisualizerRow *self,
 
   g_array_append_val (priv->lines, line_info);
 
-  if (SYSPROF_LINE_VISUALIZER_ROW_GET_CLASS (self)->counter_added)
-    SYSPROF_LINE_VISUALIZER_ROW_GET_CLASS (self)->counter_added (self, counter_id);
+  if (SYSPROF_LINE_VISUALIZER_GET_CLASS (self)->counter_added)
+    SYSPROF_LINE_VISUALIZER_GET_CLASS (self)->counter_added (self, counter_id);
 
-  sysprof_line_visualizer_row_queue_reload (self);
+  sysprof_line_visualizer_queue_reload (self);
 }
 
 void
-sysprof_line_visualizer_row_clear (SysprofLineVisualizerRow *self)
+sysprof_line_visualizer_clear (SysprofLineVisualizer *self)
 {
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
 
-  g_return_if_fail (SYSPROF_IS_LINE_VISUALIZER_ROW (self));
+  g_return_if_fail (SYSPROF_IS_LINE_VISUALIZER (self));
 
   if (priv->lines->len > 0)
     g_array_remove_range (priv->lines, 0, priv->lines->len);
@@ -580,8 +567,8 @@ calc_y_int64 (gint64 lower,
 }
 
 static gboolean
-sysprof_line_visualizer_row_load_data_frame_cb (const SysprofCaptureFrame *frame,
-                                                gpointer                   user_data)
+sysprof_line_visualizer_load_data_frame_cb (const SysprofCaptureFrame *frame,
+                                            gpointer                   user_data)
 {
   LoadData *load = user_data;
 
@@ -622,8 +609,8 @@ sysprof_line_visualizer_row_load_data_frame_cb (const SysprofCaptureFrame *frame
 }
 
 static gboolean
-sysprof_line_visualizer_row_load_data_range_cb (const SysprofCaptureFrame *frame,
-                                                gpointer                   user_data)
+sysprof_line_visualizer_load_data_range_cb (const SysprofCaptureFrame *frame,
+                                            gpointer                   user_data)
 {
   LoadData *load = user_data;
 
@@ -689,16 +676,16 @@ sysprof_line_visualizer_row_load_data_range_cb (const SysprofCaptureFrame *frame
 }
 
 static void
-sysprof_line_visualizer_row_load_data_worker (GTask        *task,
-                                              gpointer      source_object,
-                                              gpointer      task_data,
-                                              GCancellable *cancellable)
+sysprof_line_visualizer_load_data_worker (GTask        *task,
+                                          gpointer      source_object,
+                                          gpointer      task_data,
+                                          GCancellable *cancellable)
 {
   LoadData *load = task_data;
   g_autoptr(GArray) counter_ids = NULL;
 
   g_assert (G_IS_TASK (task));
-  g_assert (SYSPROF_IS_LINE_VISUALIZER_ROW (source_object));
+  g_assert (SYSPROF_IS_LINE_VISUALIZER (source_object));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   counter_ids = g_array_new (FALSE, FALSE, sizeof (guint));
@@ -716,7 +703,7 @@ sysprof_line_visualizer_row_load_data_worker (GTask        *task,
   /* If y boundaries are not set, we need to discover them by scaning the data. */
   if (!load->y_lower_set || !load->y_upper_set)
     {
-      sysprof_capture_cursor_foreach (load->cursor, sysprof_line_visualizer_row_load_data_range_cb, load);
+      sysprof_capture_cursor_foreach (load->cursor, sysprof_line_visualizer_load_data_range_cb, load);
       sysprof_capture_cursor_reset (load->cursor);
 
       /* Add extra boundary for some space above the graph line */
@@ -724,26 +711,26 @@ sysprof_line_visualizer_row_load_data_worker (GTask        *task,
         load->y_upper = load->y_upper + ((load->y_upper - load->y_lower) * .25);
     }
 
-  sysprof_capture_cursor_foreach (load->cursor, sysprof_line_visualizer_row_load_data_frame_cb, load);
+  sysprof_capture_cursor_foreach (load->cursor, sysprof_line_visualizer_load_data_frame_cb, load);
   g_task_return_pointer (task, g_steal_pointer (&load->cache), (GDestroyNotify)point_cache_unref);
 }
 
 static void
-sysprof_line_visualizer_row_load_data_async (SysprofLineVisualizerRow *self,
-                                             GCancellable             *cancellable,
-                                             GAsyncReadyCallback       callback,
-                                             gpointer                  user_data)
+sysprof_line_visualizer_load_data_async (SysprofLineVisualizer *self,
+                                         GCancellable          *cancellable,
+                                         GAsyncReadyCallback    callback,
+                                         gpointer               user_data)
 {
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
   g_autoptr(GTask) task = NULL;
   LoadData *load;
 
-  g_assert (SYSPROF_IS_LINE_VISUALIZER_ROW (self));
+  g_assert (SYSPROF_IS_LINE_VISUALIZER (self));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = g_task_new (self, cancellable, callback, user_data);
   g_task_set_priority (task, G_PRIORITY_LOW);
-  g_task_set_source_tag (task, sysprof_line_visualizer_row_load_data_async);
+  g_task_set_source_tag (task, sysprof_line_visualizer_load_data_async);
 
   if (priv->reader == NULL)
     {
@@ -773,18 +760,18 @@ sysprof_line_visualizer_row_load_data_async (SysprofLineVisualizerRow *self,
     }
 
   g_task_set_task_data  (task, load, load_data_free);
-  g_task_run_in_thread (task, sysprof_line_visualizer_row_load_data_worker);
+  g_task_run_in_thread (task, sysprof_line_visualizer_load_data_worker);
 }
 
 static PointCache *
-sysprof_line_visualizer_row_load_data_finish (SysprofLineVisualizerRow  *self,
-                                              GAsyncResult              *result,
-                                              GError                   **error)
+sysprof_line_visualizer_load_data_finish (SysprofLineVisualizer  *self,
+                                          GAsyncResult           *result,
+                                          GError                **error)
 {
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
   LoadData *state;
 
-  g_assert (SYSPROF_IS_LINE_VISUALIZER_ROW (self));
+  g_assert (SYSPROF_IS_LINE_VISUALIZER (self));
   g_assert (G_IS_TASK (result));
 
   state = g_task_get_task_data (G_TASK (result));
@@ -805,13 +792,13 @@ sysprof_line_visualizer_row_load_data_finish (SysprofLineVisualizerRow  *self,
 }
 
 void
-sysprof_line_visualizer_row_set_line_width (SysprofLineVisualizerRow *self,
-                                            guint                     counter_id,
-                                            gdouble                   width)
+sysprof_line_visualizer_set_line_width (SysprofLineVisualizer *self,
+                                        guint                  counter_id,
+                                        gdouble                width)
 {
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
 
-  g_return_if_fail (SYSPROF_IS_LINE_VISUALIZER_ROW (self));
+  g_return_if_fail (SYSPROF_IS_LINE_VISUALIZER (self));
 
   for (guint i = 0; i < priv->lines->len; i++)
     {
@@ -820,20 +807,20 @@ sysprof_line_visualizer_row_set_line_width (SysprofLineVisualizerRow *self,
       if (info->id == counter_id)
         {
           info->line_width = width;
-          sysprof_line_visualizer_row_queue_reload (self);
+          sysprof_line_visualizer_queue_reload (self);
           break;
         }
     }
 }
 
 void
-sysprof_line_visualizer_row_set_fill (SysprofLineVisualizerRow *self,
-                                      guint                     counter_id,
-                                      const GdkRGBA            *color)
+sysprof_line_visualizer_set_fill (SysprofLineVisualizer *self,
+                                  guint                  counter_id,
+                                  const GdkRGBA         *color)
 {
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
 
-  g_return_if_fail (SYSPROF_IS_LINE_VISUALIZER_ROW (self));
+  g_return_if_fail (SYSPROF_IS_LINE_VISUALIZER (self));
 
   for (guint i = 0; i < priv->lines->len; i++)
     {
@@ -844,20 +831,20 @@ sysprof_line_visualizer_row_set_fill (SysprofLineVisualizerRow *self,
           info->fill = !!color;
           if (color != NULL)
             info->background = *color;
-          sysprof_line_visualizer_row_queue_reload (self);
+          sysprof_line_visualizer_queue_reload (self);
           break;
         }
     }
 }
 
 void
-sysprof_line_visualizer_row_set_dash (SysprofLineVisualizerRow *self,
-                                      guint                     counter_id,
-                                      gboolean                  use_dash)
+sysprof_line_visualizer_set_dash (SysprofLineVisualizer *self,
+                                  guint                  counter_id,
+                                  gboolean               use_dash)
 {
-  SysprofLineVisualizerRowPrivate *priv = sysprof_line_visualizer_row_get_instance_private (self);
+  SysprofLineVisualizerPrivate *priv = sysprof_line_visualizer_get_instance_private (self);
 
-  g_return_if_fail (SYSPROF_IS_LINE_VISUALIZER_ROW (self));
+  g_return_if_fail (SYSPROF_IS_LINE_VISUALIZER (self));
 
   for (guint i = 0; i < priv->lines->len; i++)
     {
@@ -866,7 +853,7 @@ sysprof_line_visualizer_row_set_dash (SysprofLineVisualizerRow *self,
       if (info->id == counter_id)
         {
           info->use_dash = !!use_dash;
-          sysprof_line_visualizer_row_queue_reload (self);
+          sysprof_line_visualizer_queue_reload (self);
           break;
         }
     }
diff --git a/src/libsysprof-ui/sysprof-line-visualizer.h b/src/libsysprof-ui/sysprof-line-visualizer.h
new file mode 100644
index 0000000..4fac885
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-line-visualizer.h
@@ -0,0 +1,57 @@
+/* sysprof-line-visualizer.h
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "sysprof-visualizer.h"
+
+G_BEGIN_DECLS
+
+#define SYSPROF_TYPE_LINE_VISUALIZER (sysprof_line_visualizer_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (SysprofLineVisualizer, sysprof_line_visualizer, SYSPROF, LINE_VISUALIZER, 
SysprofVisualizer)
+
+struct _SysprofLineVisualizerClass
+{
+  SysprofVisualizerClass parent_class;
+
+  void (*counter_added) (SysprofLineVisualizer *self,
+                         guint                counter_id);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+GtkWidget *sysprof_line_visualizer_new            (void);
+void       sysprof_line_visualizer_clear          (SysprofLineVisualizer *self);
+void       sysprof_line_visualizer_add_counter    (SysprofLineVisualizer *self,
+                                                   guint                  counter_id,
+                                                   const GdkRGBA         *color);
+void       sysprof_line_visualizer_set_line_width (SysprofLineVisualizer *self,
+                                                   guint                  counter_id,
+                                                   gdouble                width);
+void       sysprof_line_visualizer_set_fill       (SysprofLineVisualizer *self,
+                                                   guint                  counter_id,
+                                                   const GdkRGBA         *color);
+void       sysprof_line_visualizer_set_dash       (SysprofLineVisualizer *self,
+                                                   guint                  counter_id,
+                                                   gboolean               use_dash);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-logs-aid.c b/src/libsysprof-ui/sysprof-logs-aid.c
new file mode 100644
index 0000000..466eab3
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-logs-aid.c
@@ -0,0 +1,237 @@
+/* sysprof-logs-aid.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-logs-aid"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "sysprof-color-cycle.h"
+#include "sysprof-logs-aid.h"
+#include "sysprof-logs-page.h"
+#include "sysprof-mark-visualizer.h"
+
+struct _SysprofLogsAid
+{
+  SysprofAid parent_instance;
+};
+
+typedef struct
+{
+  SysprofDisplay       *display;
+  SysprofCaptureCursor *cursor;
+  GArray               *log_marks;
+} Present;
+
+G_DEFINE_TYPE (SysprofLogsAid, sysprof_logs_aid, SYSPROF_TYPE_AID)
+
+static void
+present_free (gpointer data)
+{
+  Present *p = data;
+
+  g_clear_pointer (&p->log_marks, g_array_unref);
+  g_clear_pointer (&p->cursor, sysprof_capture_cursor_unref);
+  g_clear_object (&p->display);
+  g_slice_free (Present, p);
+}
+
+static void
+on_group_activated_cb (SysprofVisualizerGroup *group,
+                       SysprofPage            *page)
+{
+  SysprofDisplay *display;
+
+  g_assert (SYSPROF_IS_VISUALIZER_GROUP (group));
+  g_assert (SYSPROF_IS_PAGE (page));
+
+  display = SYSPROF_DISPLAY (gtk_widget_get_ancestor (GTK_WIDGET (page), SYSPROF_TYPE_DISPLAY));
+  sysprof_display_set_visible_page (display, page);
+}
+
+/**
+ * sysprof_logs_aid_new:
+ *
+ * Create a new #SysprofLogsAid.
+ *
+ * Returns: (transfer full): a newly created #SysprofLogsAid
+ */
+SysprofAid *
+sysprof_logs_aid_new (void)
+{
+  return g_object_new (SYSPROF_TYPE_LOGS_AID, NULL);
+}
+
+static gboolean
+find_marks_cb (const SysprofCaptureFrame *frame,
+               gpointer                   user_data)
+{
+  Present *p = user_data;
+
+  g_assert (frame != NULL);
+  g_assert (p != NULL);
+
+  if (frame->type == SYSPROF_CAPTURE_FRAME_LOG)
+    {
+      SysprofMarkTimeSpan span = { frame->time, frame->time };
+      g_array_append_val (p->log_marks, span);
+    }
+
+  return TRUE;
+}
+
+static gint
+compare_span (const SysprofMarkTimeSpan *a,
+              const SysprofMarkTimeSpan *b)
+{
+  if (a->kind < b->kind)
+    return -1;
+
+  if (b->kind < a->kind)
+    return 1;
+
+  if (a->begin < b->begin)
+    return -1;
+
+  if (b->begin < a->begin)
+    return 1;
+
+  if (b->end > a->end)
+    return -1;
+
+  return 0;
+}
+
+static void
+sysprof_logs_aid_present_worker (GTask        *task,
+                                  gpointer      source_object,
+                                  gpointer      task_data,
+                                  GCancellable *cancellable)
+{
+  Present *p = task_data;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (p != NULL);
+  g_assert (SYSPROF_IS_DISPLAY (p->display));
+  g_assert (p->cursor != NULL);
+  g_assert (SYSPROF_IS_LOGS_AID (source_object));
+
+  sysprof_capture_cursor_foreach (p->cursor, find_marks_cb, p);
+  g_array_sort (p->log_marks, (GCompareFunc)compare_span);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+sysprof_logs_aid_present_async (SysprofAid           *aid,
+                                SysprofCaptureReader *reader,
+                                SysprofDisplay       *display,
+                                GCancellable         *cancellable,
+                                GAsyncReadyCallback   callback,
+                                gpointer              user_data)
+{
+  static const SysprofCaptureFrameType logs[] = {
+    SYSPROF_CAPTURE_FRAME_LOG,
+  };
+  SysprofLogsAid *self = (SysprofLogsAid *)aid;
+  g_autoptr(GTask) task = NULL;
+  Present p = {0};
+
+  g_assert (SYSPROF_IS_LOGS_AID (self));
+
+  p.display = g_object_ref (display);
+  p.log_marks = g_array_new (FALSE, FALSE, sizeof (SysprofMarkTimeSpan));
+  p.cursor = sysprof_capture_cursor_new (reader);
+  sysprof_capture_cursor_add_condition (p.cursor,
+                                        sysprof_capture_condition_new_where_type_in (1, logs));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_logs_aid_present_async);
+  g_task_set_task_data (task,
+                        g_slice_dup (Present, &p),
+                        present_free);
+  g_task_run_in_thread (task, sysprof_logs_aid_present_worker);
+}
+
+static gboolean
+sysprof_logs_aid_present_finish (SysprofAid    *aid,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+  Present *p;
+
+  g_assert (SYSPROF_IS_LOGS_AID (aid));
+  g_assert (G_IS_TASK (result));
+
+  p = g_task_get_task_data (G_TASK (result));
+
+  if (p->log_marks->len > 0)
+    {
+      g_autoptr(GHashTable) items = NULL;
+      SysprofVisualizerGroup *group;
+      SysprofVisualizer *marks;
+      SysprofPage *page;
+
+      items = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
+                                     (GDestroyNotify) g_array_unref);
+      g_hash_table_insert (items, g_strdup (_("Logs")), g_array_ref (p->log_marks));
+
+      group = g_object_new (SYSPROF_TYPE_VISUALIZER_GROUP,
+                            "can-focus", TRUE,
+                            "title", _("Logs"),
+                            "visible", TRUE,
+                            NULL);
+
+      marks = sysprof_mark_visualizer_new (items);
+      sysprof_visualizer_set_title (marks, _("Logs"));
+      gtk_widget_show (GTK_WIDGET (marks));
+      sysprof_visualizer_group_insert (group, marks, 0, FALSE);
+      sysprof_display_add_group (p->display, group);
+
+      page = g_object_new (SYSPROF_TYPE_LOGS_PAGE,
+                           "title", _("Logs"),
+                           "visible", TRUE,
+                           NULL);
+      sysprof_display_add_page (p->display, page);
+
+      g_signal_connect_object (group,
+                               "group-activated",
+                               G_CALLBACK (on_group_activated_cb),
+                               page,
+                               0);
+    }
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+sysprof_logs_aid_class_init (SysprofLogsAidClass *klass)
+{
+  SysprofAidClass *aid_class = SYSPROF_AID_CLASS (klass);
+
+  aid_class->present_async = sysprof_logs_aid_present_async;
+  aid_class->present_finish = sysprof_logs_aid_present_finish;
+}
+
+static void
+sysprof_logs_aid_init (SysprofLogsAid *self)
+{
+}
diff --git a/src/libsysprof-ui/sysprof-logs-aid.h b/src/libsysprof-ui/sysprof-logs-aid.h
new file mode 100644
index 0000000..2f2d13a
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-logs-aid.h
@@ -0,0 +1,33 @@
+/* sysprof-logs-aid.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "sysprof-aid.h"
+
+G_BEGIN_DECLS
+
+#define SYSPROF_TYPE_LOGS_AID (sysprof_logs_aid_get_type())
+
+G_DECLARE_FINAL_TYPE (SysprofLogsAid, sysprof_logs_aid, SYSPROF, LOGS_AID, SysprofAid)
+
+SysprofAid *sysprof_logs_aid_new (void);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-logs-page.c b/src/libsysprof-ui/sysprof-logs-page.c
new file mode 100644
index 0000000..f2b46af
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-logs-page.c
@@ -0,0 +1,116 @@
+/* sysprof-logs-page.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-logs-page"
+
+#include "config.h"
+
+#include "sysprof-log-model.h"
+#include "sysprof-logs-page.h"
+
+struct _SysprofLogsPage
+{
+  SysprofPage parent_instance;
+
+  /* Template Widgets */
+  GtkTreeView *tree_view;
+};
+
+G_DEFINE_TYPE (SysprofLogsPage, sysprof_logs_page, SYSPROF_TYPE_PAGE)
+
+static void
+sysprof_logs_page_load_cb (GObject      *object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  SysprofLogsPage *self;
+  g_autoptr(SysprofLogModel) model = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GTask) task = user_data;
+
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!(model = sysprof_log_model_new_finish (result, &error)))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_boolean (task, TRUE);
+
+  self = g_task_get_source_object (task);
+
+  gtk_tree_view_set_model (self->tree_view, GTK_TREE_MODEL (model));
+}
+
+static void
+sysprof_logs_page_load_async (SysprofPage             *page,
+                              SysprofCaptureReader    *reader,
+                              SysprofSelection        *selection,
+                              SysprofCaptureCondition *filter,
+                              GCancellable            *cancellable,
+                              GAsyncReadyCallback      callback,
+                              gpointer                 user_data)
+{
+  SysprofLogsPage *self = (SysprofLogsPage *)page;
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (SYSPROF_IS_LOGS_PAGE (self));
+  g_assert (reader != NULL);
+  g_assert (!selection || SYSPROF_IS_SELECTION (selection));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_logs_page_load_async);
+
+  sysprof_log_model_new_async (reader,
+                               selection,
+                               cancellable,
+                               sysprof_logs_page_load_cb,
+                               g_steal_pointer (&task));
+}
+
+static gboolean
+sysprof_logs_page_load_finish (SysprofPage   *page,
+                               GAsyncResult  *result,
+                               GError       **error)
+{
+  g_assert (SYSPROF_IS_LOGS_PAGE (page));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+sysprof_logs_page_class_init (SysprofLogsPageClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  SysprofPageClass *page_class = SYSPROF_PAGE_CLASS (klass);
+
+  page_class->load_async = sysprof_logs_page_load_async;
+  page_class->load_finish = sysprof_logs_page_load_finish;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/sysprof/ui/sysprof-logs-page.ui");
+  gtk_widget_class_bind_template_child (widget_class, SysprofLogsPage, tree_view);
+}
+
+static void
+sysprof_logs_page_init (SysprofLogsPage *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/src/libsysprof-ui/sysprof-visualizer-row-private.h b/src/libsysprof-ui/sysprof-logs-page.h
similarity index 74%
rename from src/libsysprof-ui/sysprof-visualizer-row-private.h
rename to src/libsysprof-ui/sysprof-logs-page.h
index a364056..97bb245 100644
--- a/src/libsysprof-ui/sysprof-visualizer-row-private.h
+++ b/src/libsysprof-ui/sysprof-logs-page.h
@@ -1,6 +1,6 @@
-/* sysprof-visualizer-row-private.h
+/* sysprof-logs-page.h
  *
- * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,10 +20,12 @@
 
 #pragma once
 
-#include "sysprof-visualizer-row.h"
+#include "sysprof-page.h"
 
 G_BEGIN_DECLS
 
-gint _sysprof_visualizer_row_get_graph_width (SysprofVisualizerRow *self);
+#define SYSPROF_TYPE_LOGS_PAGE (sysprof_logs_page_get_type())
+
+G_DECLARE_FINAL_TYPE (SysprofLogsPage, sysprof_logs_page, SYSPROF, LOGS_PAGE, SysprofPage)
 
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-logs-page.ui b/src/libsysprof-ui/sysprof-logs-page.ui
new file mode 100644
index 0000000..6760253
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-logs-page.ui
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="SysprofLogsPage" parent="SysprofPage">
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkTreeView" id="tree_view">
+            <property name="tooltip-column">3</property>
+            <property name="headers-visible">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkTreeViewColumn">
+                <property name="expand">false</property>
+                <property name="title" translatable="yes">Time</property>
+                <child>
+                  <object class="GtkCellRendererText">
+                    <property name="xalign">0.0</property>
+                  </object>
+                  <attributes>
+                    <attribute name="text">4</attribute>
+                  </attributes>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkTreeViewColumn">
+                <property name="expand">false</property>
+                <property name="title" translatable="yes">Severity</property>
+                <child>
+                  <object class="GtkCellRendererText">
+                    <property name="xalign">0.0</property>
+                  </object>
+                  <attributes>
+                    <attribute name="text">1</attribute>
+                  </attributes>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkTreeViewColumn">
+                <property name="expand">false</property>
+                <property name="resizable">true</property>
+                <property name="title" translatable="yes">Domain</property>
+                <child>
+                  <object class="GtkCellRendererText">
+                    <property name="xalign">0.0</property>
+                  </object>
+                  <attributes>
+                    <attribute name="text">2</attribute>
+                  </attributes>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkTreeViewColumn">
+                <property name="expand">true</property>
+                <property name="title" translatable="yes">Message</property>
+                <child>
+                  <object class="GtkCellRendererText">
+                    <property name="ellipsize">end</property>
+                    <property name="xalign">0.0</property>
+                  </object>
+                  <attributes>
+                    <attribute name="text">3</attribute>
+                  </attributes>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libsysprof-ui/sysprof-mark-visualizer.c b/src/libsysprof-ui/sysprof-mark-visualizer.c
new file mode 100644
index 0000000..216cf62
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-mark-visualizer.c
@@ -0,0 +1,287 @@
+/* sysprof-mark-visualizer.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-mark-visualizer"
+
+#include "config.h"
+
+#include "sysprof-mark-visualizer.h"
+
+#define RECT_HEIGHT    (4)
+#define RECT_MIN_WIDTH (3)
+#define RECT_OVERLAP   (-1)
+
+struct _SysprofMarkVisualizer
+{
+  SysprofVisualizer  parent_instance;
+  GHashTable        *spans_by_group;
+  GHashTable        *rgba_by_group;
+  GHashTable        *rgba_by_kind;
+  GHashTable        *row_by_kind;
+  guint              x_is_dirty : 1;
+};
+
+G_DEFINE_TYPE (SysprofMarkVisualizer, sysprof_mark_visualizer, SYSPROF_TYPE_VISUALIZER)
+
+static void
+reset_positions (SysprofMarkVisualizer *self)
+{
+  g_assert (SYSPROF_IS_MARK_VISUALIZER (self));
+
+  self->x_is_dirty = TRUE;
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+SysprofVisualizer *
+sysprof_mark_visualizer_new (GHashTable *groups)
+{
+  SysprofMarkVisualizer *self;
+  guint n_items;
+  gint height;
+
+  g_return_val_if_fail (groups != NULL, NULL);
+
+  self = g_object_new (SYSPROF_TYPE_MARK_VISUALIZER, NULL);
+  self->spans_by_group = g_hash_table_ref (groups);
+
+  reset_positions (self);
+
+  n_items = g_hash_table_size (groups);
+  height = MAX (35, n_items * (RECT_HEIGHT - RECT_OVERLAP));
+  gtk_widget_set_size_request (GTK_WIDGET (self), -1, height);
+
+  return SYSPROF_VISUALIZER (g_steal_pointer (&self));
+}
+
+static gboolean
+sysprof_mark_visualizer_draw (GtkWidget *widget,
+                              cairo_t   *cr)
+{
+  SysprofMarkVisualizer *self = (SysprofMarkVisualizer *)widget;
+  SysprofVisualizer *vis = (SysprofVisualizer *)widget;
+  GHashTableIter iter;
+  GtkAllocation alloc;
+  gpointer k, v;
+  gboolean ret;
+  gint n_groups = 0;
+  gint y = 0;
+
+  g_assert (SYSPROF_IS_MARK_VISUALIZER (self));
+  g_assert (cr != NULL);
+
+  ret = GTK_WIDGET_CLASS (sysprof_mark_visualizer_parent_class)->draw (widget, cr);
+  if (self->spans_by_group == NULL)
+    return ret;
+
+  gtk_widget_get_allocation (widget, &alloc);
+
+  /* Pre-calculate all time slots so we can join later */
+  if (self->x_is_dirty)
+    {
+      g_hash_table_iter_init (&iter, self->spans_by_group);
+      while (g_hash_table_iter_next (&iter, &k, &v))
+        {
+          const GArray *spans = v;
+
+          for (guint i = 0; i < spans->len; i++)
+            {
+              SysprofMarkTimeSpan *span = &g_array_index (spans, SysprofMarkTimeSpan, i);
+
+              span->x = sysprof_visualizer_get_x_for_time (vis, span->begin);
+              span->x2 = sysprof_visualizer_get_x_for_time (vis, span->end);
+            }
+        }
+
+      self->x_is_dirty = FALSE;
+    }
+
+  n_groups = g_hash_table_size (self->spans_by_group);
+
+  g_hash_table_iter_init (&iter, self->spans_by_group);
+  while (g_hash_table_iter_next (&iter, &k, &v))
+    {
+      static const GdkRGBA black = {0, 0, 0, 1};
+      SysprofMarkTimeSpan *span;
+      const gchar *group = k;
+      const GArray *spans = v;
+      const GdkRGBA *rgba;
+      const GdkRGBA *kindrgba;
+      const GdkRGBA *grouprgba;
+
+      if ((grouprgba = g_hash_table_lookup (self->rgba_by_group, group)))
+        {
+          rgba = grouprgba;
+          gdk_cairo_set_source_rgba (cr, rgba);
+        }
+
+      for (guint i = 0; i < spans->len; i++)
+        {
+          gint x1, x2;
+
+          span = &g_array_index (spans, SysprofMarkTimeSpan, i);
+
+          if (n_groups == 1)
+            {
+              rgba = &black;
+              if ((kindrgba = g_hash_table_lookup (self->rgba_by_kind, GUINT_TO_POINTER (span->kind))))
+                rgba = kindrgba;
+              else if ((grouprgba = g_hash_table_lookup (self->rgba_by_group, group)))
+                rgba = grouprgba;
+              gdk_cairo_set_source_rgba (cr, rgba);
+            }
+
+          x1 = span->x;
+          x2 = x1 + RECT_MIN_WIDTH;
+
+          if (span->x2 > x2)
+            x2 = span->x2;
+
+          /* If we are limited to one group, we might need to get the row
+           * height for the kind of span this is.
+           */
+          if (n_groups == 1)
+            {
+              gint row = GPOINTER_TO_INT (g_hash_table_lookup (self->row_by_kind, GUINT_TO_POINTER 
(span->kind)));
+              y = row * (RECT_HEIGHT - RECT_OVERLAP);
+            }
+
+          for (guint j = i + 1; j < spans->len; j++)
+            {
+              const SysprofMarkTimeSpan *next = &g_array_index (spans, SysprofMarkTimeSpan, j);
+
+              /* Don't join this if we are about to draw a different kind */
+              if (n_groups == 1 && next->kind != span->kind)
+                break;
+
+              if (next->x <= x2)
+                {
+                  x2 = MAX (x2, next->x2);
+                  i++;
+                  continue;
+                }
+
+              break;
+            }
+
+          cairo_rectangle (cr, x1, y, x2 - x1, RECT_HEIGHT);
+
+          if (n_groups == 1)
+            cairo_fill (cr);
+        }
+
+      if (n_groups > 1)
+        cairo_fill (cr);
+
+      y += RECT_HEIGHT + RECT_OVERLAP;
+    }
+
+  return ret;
+}
+
+static void
+sysprof_mark_visualizer_size_allocate (GtkWidget     *widget,
+                                       GtkAllocation *alloc)
+{
+  SysprofMarkVisualizer *self = (SysprofMarkVisualizer *)widget;
+
+  g_assert (SYSPROF_IS_MARK_VISUALIZER (self));
+  g_assert (alloc != NULL);
+
+  GTK_WIDGET_CLASS (sysprof_mark_visualizer_parent_class)->size_allocate (widget, alloc);
+
+  reset_positions (self);
+}
+
+static void
+sysprof_mark_visualizer_finalize (GObject *object)
+{
+  SysprofMarkVisualizer *self = (SysprofMarkVisualizer *)object;
+
+  g_clear_pointer (&self->spans_by_group, g_hash_table_unref);
+  g_clear_pointer (&self->rgba_by_group, g_hash_table_unref);
+  g_clear_pointer (&self->rgba_by_kind, g_hash_table_unref);
+  g_clear_pointer (&self->row_by_kind, g_hash_table_unref);
+
+  G_OBJECT_CLASS (sysprof_mark_visualizer_parent_class)->finalize (object);
+}
+
+static void
+sysprof_mark_visualizer_class_init (SysprofMarkVisualizerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = sysprof_mark_visualizer_finalize;
+
+  widget_class->draw = sysprof_mark_visualizer_draw;
+  widget_class->size_allocate = sysprof_mark_visualizer_size_allocate;
+}
+
+static void
+sysprof_mark_visualizer_init (SysprofMarkVisualizer *self)
+{
+  self->rgba_by_kind = g_hash_table_new_full (NULL, NULL, NULL, g_free);
+  self->row_by_kind = g_hash_table_new (NULL, NULL);
+  self->rgba_by_group = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+}
+
+void
+sysprof_mark_visualizer_set_group_rgba (SysprofMarkVisualizer *self,
+                                        const gchar           *group,
+                                        const GdkRGBA         *rgba)
+{
+  g_return_if_fail (SYSPROF_IS_MARK_VISUALIZER (self));
+  g_return_if_fail (group != NULL);
+
+  g_hash_table_insert (self->rgba_by_group,
+                       g_strdup (group),
+                       g_memdup (rgba, sizeof *rgba));
+}
+
+void
+sysprof_mark_visualizer_set_kind_rgba (SysprofMarkVisualizer *self,
+                                       GHashTable            *rgba_by_kind)
+{
+  g_return_if_fail (SYSPROF_IS_MARK_VISUALIZER (self));
+
+  if (rgba_by_kind != self->rgba_by_kind)
+    {
+      g_hash_table_remove_all (self->row_by_kind);
+
+      g_clear_pointer (&self->rgba_by_kind, g_hash_table_unref);
+
+      if (rgba_by_kind)
+        {
+          GHashTableIter iter;
+          guint row = 0;
+          gpointer k;
+
+          self->rgba_by_kind = g_hash_table_ref (rgba_by_kind);
+
+          g_hash_table_iter_init (&iter, rgba_by_kind);
+          while (g_hash_table_iter_next (&iter, &k, NULL))
+            g_hash_table_insert (self->row_by_kind, k, GUINT_TO_POINTER (row++));
+
+          gtk_widget_set_size_request (GTK_WIDGET (self),
+                                       -1,
+                                       MAX (35, row * (RECT_HEIGHT - RECT_OVERLAP)));
+        }
+    }
+}
diff --git a/src/libsysprof-ui/sysprof-mark-visualizer.h b/src/libsysprof-ui/sysprof-mark-visualizer.h
new file mode 100644
index 0000000..36327e1
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-mark-visualizer.h
@@ -0,0 +1,47 @@
+/* sysprof-mark-visualizer.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "sysprof-visualizer.h"
+
+G_BEGIN_DECLS
+
+typedef struct
+{
+  gint64 begin;
+  gint64 end;
+  guint  kind;
+  gint   x;
+  gint   x2;
+} SysprofMarkTimeSpan;
+
+#define SYSPROF_TYPE_MARK_VISUALIZER (sysprof_mark_visualizer_get_type())
+
+G_DECLARE_FINAL_TYPE (SysprofMarkVisualizer, sysprof_mark_visualizer, SYSPROF, MARK_VISUALIZER, 
SysprofVisualizer)
+
+SysprofVisualizer *sysprof_mark_visualizer_new            (GHashTable            *groups);
+void               sysprof_mark_visualizer_set_group_rgba (SysprofMarkVisualizer *self,
+                                                           const gchar           *group,
+                                                           const GdkRGBA         *rgba);
+void               sysprof_mark_visualizer_set_kind_rgba  (SysprofMarkVisualizer *self,
+                                                           GHashTable            *rgba_by_kind);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-marks-aid.c b/src/libsysprof-ui/sysprof-marks-aid.c
new file mode 100644
index 0000000..a8f79b4
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-marks-aid.c
@@ -0,0 +1,319 @@
+/* sysprof-marks-aid.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-marks-aid"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "sysprof-color-cycle.h"
+#include "sysprof-marks-aid.h"
+#include "sysprof-marks-page.h"
+#include "sysprof-mark-visualizer.h"
+
+struct _SysprofMarksAid
+{
+  SysprofAid parent_instance;
+};
+
+typedef struct
+{
+  SysprofDisplay       *display;
+  SysprofCaptureCursor *cursor;
+  GHashTable           *categories;
+  GHashTable           *kinds;
+  guint                 last_kind;
+  guint                 has_marks : 1;
+} Present;
+
+G_DEFINE_TYPE (SysprofMarksAid, sysprof_marks_aid, SYSPROF_TYPE_AID)
+
+static void
+present_free (gpointer data)
+{
+  Present *p = data;
+
+  g_clear_pointer (&p->categories, g_hash_table_unref);
+  g_clear_pointer (&p->kinds, g_hash_table_unref);
+  g_clear_pointer (&p->cursor, sysprof_capture_cursor_unref);
+  g_clear_object (&p->display);
+  g_slice_free (Present, p);
+}
+
+static void
+on_group_activated_cb (SysprofVisualizerGroup *group,
+                       SysprofPage            *page)
+{
+  SysprofDisplay *display;
+
+  g_assert (SYSPROF_IS_VISUALIZER_GROUP (group));
+  g_assert (SYSPROF_IS_PAGE (page));
+
+  display = SYSPROF_DISPLAY (gtk_widget_get_ancestor (GTK_WIDGET (page), SYSPROF_TYPE_DISPLAY));
+  sysprof_display_set_visible_page (display, page);
+}
+
+/**
+ * sysprof_marks_aid_new:
+ *
+ * Create a new #SysprofMarksAid.
+ *
+ * Returns: (transfer full): a newly created #SysprofMarksAid
+ */
+SysprofAid *
+sysprof_marks_aid_new (void)
+{
+  return g_object_new (SYSPROF_TYPE_MARKS_AID, NULL);
+}
+
+static gboolean
+find_marks_cb (const SysprofCaptureFrame *frame,
+               gpointer                   user_data)
+{
+  Present *p = user_data;
+
+  g_assert (frame != NULL);
+  g_assert (p != NULL);
+
+  if (frame->type == SYSPROF_CAPTURE_FRAME_MARK)
+    {
+      const SysprofCaptureMark *mark = (const SysprofCaptureMark *)frame;
+      SysprofMarkTimeSpan span = { frame->time, frame->time + mark->duration };
+      gchar joined[64];
+      gpointer kptr;
+      GArray *items;
+
+      p->has_marks = TRUE;
+
+      if G_UNLIKELY (!(items = g_hash_table_lookup (p->categories, mark->group)))
+        {
+          items = g_array_new (FALSE, FALSE, sizeof (SysprofMarkTimeSpan));
+          g_hash_table_insert (p->categories, g_strdup (mark->group), items);
+        }
+
+      g_snprintf (joined, sizeof joined, "%s:%s", mark->group, mark->name);
+
+      if G_UNLIKELY (!(kptr = g_hash_table_lookup (p->kinds, joined)))
+        {
+          p->last_kind++;
+          kptr = GINT_TO_POINTER (p->last_kind);
+          g_hash_table_insert (p->kinds, g_strdup (joined), kptr);
+        }
+
+      span.kind = GPOINTER_TO_INT (kptr);
+
+      g_array_append_val (items, span);
+    }
+
+  return TRUE;
+}
+
+static gint
+compare_span (const SysprofMarkTimeSpan *a,
+              const SysprofMarkTimeSpan *b)
+{
+  if (a->kind < b->kind)
+    return -1;
+
+  if (b->kind < a->kind)
+    return 1;
+
+  if (a->begin < b->begin)
+    return -1;
+
+  if (b->begin < a->begin)
+    return 1;
+
+  if (b->end > a->end)
+    return -1;
+
+  return 0;
+}
+
+static void
+sysprof_marks_aid_present_worker (GTask        *task,
+                                  gpointer      source_object,
+                                  gpointer      task_data,
+                                  GCancellable *cancellable)
+{
+  Present *p = task_data;
+  GHashTableIter iter;
+  gpointer k, v;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (p != NULL);
+  g_assert (SYSPROF_IS_DISPLAY (p->display));
+  g_assert (p->cursor != NULL);
+  g_assert (SYSPROF_IS_MARKS_AID (source_object));
+
+  sysprof_capture_cursor_foreach (p->cursor, find_marks_cb, p);
+
+  g_hash_table_iter_init (&iter, p->categories);
+  while (g_hash_table_iter_next (&iter, &k, &v))
+    {
+      GArray *spans = v;
+
+      g_array_sort (spans, (GCompareFunc)compare_span);
+    }
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+sysprof_marks_aid_present_async (SysprofAid           *aid,
+                                 SysprofCaptureReader *reader,
+                                 SysprofDisplay       *display,
+                                 GCancellable         *cancellable,
+                                 GAsyncReadyCallback   callback,
+                                 gpointer              user_data)
+{
+  static const SysprofCaptureFrameType marks[] = {
+    SYSPROF_CAPTURE_FRAME_MARK,
+  };
+  SysprofMarksAid *self = (SysprofMarksAid *)aid;
+  g_autoptr(GTask) task = NULL;
+  Present p = {0};
+
+  g_assert (SYSPROF_IS_MARKS_AID (self));
+
+  p.display = g_object_ref (display);
+  p.categories = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
+                                        (GDestroyNotify) g_array_unref);
+  p.kinds = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+  p.cursor = sysprof_capture_cursor_new (reader);
+  sysprof_capture_cursor_add_condition (p.cursor,
+                                        sysprof_capture_condition_new_where_type_in (1, marks));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_marks_aid_present_async);
+  g_task_set_task_data (task,
+                        g_slice_dup (Present, &p),
+                        present_free);
+  g_task_run_in_thread (task, sysprof_marks_aid_present_worker);
+}
+
+static gboolean
+sysprof_marks_aid_present_finish (SysprofAid    *aid,
+                                  GAsyncResult  *result,
+                                  GError       **error)
+{
+  Present *p;
+
+  g_assert (SYSPROF_IS_MARKS_AID (aid));
+  g_assert (G_IS_TASK (result));
+
+  p = g_task_get_task_data (G_TASK (result));
+
+  if (p->has_marks)
+    {
+      g_autoptr(SysprofColorCycle) cycle = sysprof_color_cycle_new ();
+      SysprofVisualizerGroup *group;
+      SysprofVisualizer *marks;
+      SysprofPage *page;
+      GHashTableIter iter;
+      gpointer k, v;
+
+      group = g_object_new (SYSPROF_TYPE_VISUALIZER_GROUP,
+                            "can-focus", TRUE,
+                            "has-page", TRUE,
+                            "title", _("Timings"),
+                            "visible", TRUE,
+                            NULL);
+
+      marks = sysprof_mark_visualizer_new (p->categories);
+      sysprof_visualizer_set_title (marks, _("Timings"));
+      gtk_widget_show (GTK_WIDGET (marks));
+
+      g_hash_table_iter_init (&iter, p->categories);
+      while (g_hash_table_iter_next (&iter, &k, &v))
+        {
+          g_autoptr(GHashTable) seen = g_hash_table_new_full (NULL, NULL, NULL, g_free);
+          g_autoptr(GHashTable) scoped = NULL;
+          SysprofVisualizer *scoped_vis;
+          GArray *spans = v;
+          const gchar *name = k;
+          GdkRGBA rgba;
+          GdkRGBA kind_rgba;
+          gdouble ratio;
+
+          sysprof_color_cycle_next (cycle, &rgba);
+          sysprof_mark_visualizer_set_group_rgba (SYSPROF_MARK_VISUALIZER (marks), name, &rgba);
+
+          /* Now make a scoped row just for this group */
+          scoped = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
+                                          (GDestroyNotify)g_array_unref);
+          g_hash_table_insert (scoped, g_strdup (name), g_array_ref (spans));
+
+          scoped_vis = sysprof_mark_visualizer_new (scoped);
+          sysprof_visualizer_set_title (scoped_vis, name);
+          sysprof_mark_visualizer_set_group_rgba (SYSPROF_MARK_VISUALIZER (scoped_vis), name, &rgba);
+          sysprof_visualizer_group_insert (group, scoped_vis, -1, TRUE);
+
+          ratio = .4 / p->last_kind;
+
+          for (guint i = 0; i < spans->len; i++)
+            {
+              const SysprofMarkTimeSpan *span = &g_array_index (spans, SysprofMarkTimeSpan, i);
+
+              if (!g_hash_table_contains (seen, GUINT_TO_POINTER (span->kind)))
+                {
+                  dzl_rgba_shade (&rgba, &kind_rgba, 1 + (ratio * span->kind));
+                  g_hash_table_insert (seen,
+                                       GUINT_TO_POINTER (span->kind),
+                                       g_memdup (&kind_rgba, sizeof kind_rgba));
+                }
+            }
+
+          sysprof_mark_visualizer_set_kind_rgba (SYSPROF_MARK_VISUALIZER (scoped_vis), seen);
+        }
+
+      page = g_object_new (SYSPROF_TYPE_MARKS_PAGE,
+                           "zoom-manager", sysprof_display_get_zoom_manager (p->display),
+                           "visible", TRUE,
+                           NULL);
+
+      g_signal_connect_object (group,
+                               "group-activated",
+                               G_CALLBACK (on_group_activated_cb),
+                               page,
+                               0);
+
+      sysprof_visualizer_group_insert (group, marks, 0, FALSE);
+      sysprof_display_add_group (p->display, group);
+      sysprof_display_add_page (p->display, page);
+    }
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+sysprof_marks_aid_class_init (SysprofMarksAidClass *klass)
+{
+  SysprofAidClass *aid_class = SYSPROF_AID_CLASS (klass);
+
+  aid_class->present_async = sysprof_marks_aid_present_async;
+  aid_class->present_finish = sysprof_marks_aid_present_finish;
+}
+
+static void
+sysprof_marks_aid_init (SysprofMarksAid *self)
+{
+}
diff --git a/src/libsysprof-ui/sysprof-marks-aid.h b/src/libsysprof-ui/sysprof-marks-aid.h
new file mode 100644
index 0000000..f201f3a
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-marks-aid.h
@@ -0,0 +1,33 @@
+/* sysprof-marks-aid.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "sysprof-aid.h"
+
+G_BEGIN_DECLS
+
+#define SYSPROF_TYPE_MARKS_AID (sysprof_marks_aid_get_type())
+
+G_DECLARE_FINAL_TYPE (SysprofMarksAid, sysprof_marks_aid, SYSPROF, MARKS_AID, SysprofAid)
+
+SysprofAid *sysprof_marks_aid_new (void);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-marks-view.c b/src/libsysprof-ui/sysprof-marks-page.c
similarity index 64%
rename from src/libsysprof-ui/sysprof-marks-view.c
rename to src/libsysprof-ui/sysprof-marks-page.c
index c09d71d..3c4ce2f 100644
--- a/src/libsysprof-ui/sysprof-marks-view.c
+++ b/src/libsysprof-ui/sysprof-marks-page.c
@@ -1,4 +1,4 @@
-/* sysprof-marks-view.c
+/* sysprof-marks-page.c
  *
  * Copyright 2019 Christian Hergert <chergert redhat com>
  *
@@ -18,13 +18,13 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#define G_LOG_DOMAIN "sysprof-marks-view"
+#define G_LOG_DOMAIN "sysprof-marks-page"
 
 #include "config.h"
 
 #include "sysprof-cell-renderer-duration.h"
 #include "sysprof-marks-model.h"
-#include "sysprof-marks-view.h"
+#include "sysprof-marks-page.h"
 #include "sysprof-ui-private.h"
 #include "sysprof-zoom-manager.h"
 
@@ -42,10 +42,16 @@ typedef struct
   /* Template objects */
   GtkScrolledWindow           *scroller;
   GtkTreeView                 *tree_view;
+  GtkBox                      *details_box;
   GtkTreeViewColumn           *duration_column;
   SysprofCellRendererDuration *duration_cell;
   GtkStack                    *stack;
-} SysprofMarksViewPrivate;
+  GtkLabel                    *group;
+  GtkLabel                    *mark;
+  GtkLabel                    *time;
+  GtkLabel                    *duration;
+  GtkTextView                 *message;
+} SysprofMarksPagePrivate;
 
 enum {
   PROP_0,
@@ -56,17 +62,17 @@ enum {
 
 static GParamSpec *properties [N_PROPS];
 
-G_DEFINE_TYPE_WITH_PRIVATE (SysprofMarksView, sysprof_marks_view, GTK_TYPE_BIN)
+G_DEFINE_TYPE_WITH_PRIVATE (SysprofMarksPage, sysprof_marks_page, SYSPROF_TYPE_PAGE)
 
 static gboolean
-sysprof_marks_view_tree_view_key_press_event_cb (SysprofMarksView  *self,
+sysprof_marks_page_tree_view_key_press_event_cb (SysprofMarksPage  *self,
                                                  const GdkEventKey *key,
                                                  GtkTreeView       *tree_view)
 {
-  SysprofMarksViewPrivate *priv = sysprof_marks_view_get_instance_private (self);
+  SysprofMarksPagePrivate *priv = sysprof_marks_page_get_instance_private (self);
   gint dir = 0;
 
-  g_assert (SYSPROF_MARKS_VIEW (self));
+  g_assert (SYSPROF_MARKS_PAGE (self));
   g_assert (key != NULL);
 
   if (key->state == 0)
@@ -127,21 +133,29 @@ get_first_selected (GtkTreeSelection  *selection,
 }
 
 static void
-sysprof_marks_view_selection_changed_cb (SysprofMarksView *self,
+sysprof_marks_page_selection_changed_cb (SysprofMarksPage *self,
                                          GtkTreeSelection *selection)
 {
-  SysprofMarksViewPrivate *priv = sysprof_marks_view_get_instance_private (self);
+  SysprofMarksPagePrivate *priv = sysprof_marks_page_get_instance_private (self);
   GtkTreeModel *model;
   GtkTreeIter iter;
 
-  g_assert (SYSPROF_IS_MARKS_VIEW (self));
+  g_assert (SYSPROF_IS_MARKS_PAGE (self));
   g_assert (GTK_IS_TREE_SELECTION (selection));
 
   if (get_first_selected (selection, &model, &iter))
     {
+      g_autofree gchar *group = NULL;
+      g_autofree gchar *name = NULL;
+      g_autofree gchar *duration_str = NULL;
+      g_autofree gchar *time_str = NULL;
+      g_autofree gchar *text = NULL;
       GtkAdjustment *adj;
       gdouble x;
       gint64 begin_time;
+      gint64 end_time;
+      gint64 duration;
+      gint64 otime;
       gdouble lower;
       gdouble upper;
       gdouble value;
@@ -149,9 +163,26 @@ sysprof_marks_view_selection_changed_cb (SysprofMarksView *self,
       gint width;
 
       gtk_tree_model_get (model, &iter,
+                          SYSPROF_MARKS_MODEL_COLUMN_GROUP, &group,
+                          SYSPROF_MARKS_MODEL_COLUMN_NAME, &name,
                           SYSPROF_MARKS_MODEL_COLUMN_BEGIN_TIME, &begin_time,
+                          SYSPROF_MARKS_MODEL_COLUMN_END_TIME, &end_time,
+                          SYSPROF_MARKS_MODEL_COLUMN_TEXT, &text,
                           -1);
 
+      duration = end_time - begin_time;
+      duration_str = _sysprof_format_duration (duration);
+
+      otime = begin_time - priv->capture_begin_time;
+      time_str = _sysprof_format_duration (otime);
+
+      gtk_label_set_label (priv->group, group);
+      gtk_label_set_label (priv->mark, name);
+      gtk_label_set_label (priv->duration, duration_str);
+      gtk_label_set_label (priv->time, time_str);
+
+      gtk_text_buffer_set_text (gtk_text_view_get_buffer (priv->message), text, -1);
+
       adj = gtk_scrolled_window_get_hadjustment (priv->scroller);
       width = gtk_tree_view_column_get_width (priv->duration_column);
       x = sysprof_zoom_manager_get_offset_at_time (priv->zoom_manager,
@@ -173,19 +204,19 @@ sysprof_marks_view_selection_changed_cb (SysprofMarksView *self,
 }
 
 static gboolean
-sysprof_marks_view_tree_view_query_tooltip_cb (SysprofMarksView *self,
+sysprof_marks_page_tree_view_query_tooltip_cb (SysprofMarksPage *self,
                                                gint              x,
                                                gint              y,
                                                gboolean          keyboard_mode,
                                                GtkTooltip       *tooltip,
                                                GtkTreeView      *tree_view)
 {
-  SysprofMarksViewPrivate *priv = sysprof_marks_view_get_instance_private (self);
+  SysprofMarksPagePrivate *priv = sysprof_marks_page_get_instance_private (self);
   g_autoptr(GtkTreePath) path = NULL;
   GtkTreeViewColumn *column;
   gint cell_x, cell_y;
 
-  g_assert (SYSPROF_IS_MARKS_VIEW (self));
+  g_assert (SYSPROF_IS_MARKS_PAGE (self));
   g_assert (GTK_IS_TOOLTIP (tooltip));
   g_assert (GTK_IS_TREE_VIEW (tree_view));
 
@@ -229,24 +260,139 @@ sysprof_marks_view_tree_view_query_tooltip_cb (SysprofMarksView *self,
 }
 
 static void
-sysprof_marks_view_finalize (GObject *object)
+sysprof_marks_page_load_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
 {
-  SysprofMarksView *self = (SysprofMarksView *)object;
-  SysprofMarksViewPrivate *priv = sysprof_marks_view_get_instance_private (self);
+  g_autoptr(SysprofMarksModel) model = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GTask) task = user_data;
+  SysprofMarksPagePrivate *priv;
+  SysprofCaptureReader *reader;
+  SysprofMarksPage *self;
+
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  priv = sysprof_marks_page_get_instance_private (self);
+
+  if (!(model = sysprof_marks_model_new_finish (result, &error)))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  reader = g_task_get_task_data (task);
+  g_assert (reader != NULL);
+
+  priv->capture_begin_time = sysprof_capture_reader_get_start_time (reader);
+  priv->capture_end_time = sysprof_capture_reader_get_end_time (reader);
+
+  g_object_set (priv->duration_cell,
+                "capture-begin-time", priv->capture_begin_time,
+                "capture-end-time", priv->capture_end_time,
+                "zoom-manager", priv->zoom_manager,
+                NULL);
+
+  gtk_tree_view_set_model (priv->tree_view, GTK_TREE_MODEL (model));
+
+  if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (model), NULL) == 0)
+    gtk_stack_set_visible_child_name (priv->stack, "empty-state");
+  else
+    gtk_stack_set_visible_child_name (priv->stack, "marks");
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+sysprof_marks_page_load_async (SysprofPage             *page,
+                               SysprofCaptureReader    *reader,
+                               SysprofSelection        *selection,
+                               SysprofCaptureCondition *filter,
+                               GCancellable            *cancellable,
+                               GAsyncReadyCallback      callback,
+                               gpointer                 user_data)
+{
+  SysprofMarksPage *self = (SysprofMarksPage *)page;
+  SysprofMarksPagePrivate *priv = sysprof_marks_page_get_instance_private (self);
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (SYSPROF_IS_MARKS_PAGE (self));
+  g_return_if_fail (reader != NULL);
+  g_return_if_fail (!selection || SYSPROF_IS_SELECTION (selection));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_marks_page_load_async);
+  g_task_set_task_data (task,
+                        sysprof_capture_reader_ref (reader),
+                        (GDestroyNotify) sysprof_capture_reader_unref);
+
+  sysprof_marks_model_new_async (reader,
+                                 priv->kind,
+                                 selection,
+                                 cancellable,
+                                 sysprof_marks_page_load_cb,
+                                 g_steal_pointer (&task));
+}
+
+static gboolean
+sysprof_marks_page_load_finish (SysprofPage   *page,
+                                GAsyncResult  *result,
+                                GError       **error)
+{
+  g_return_val_if_fail (SYSPROF_IS_MARKS_PAGE (page), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+sysprof_marks_page_set_hadjustment (SysprofPage   *page,
+                                    GtkAdjustment *hadjustment)
+{
+  SysprofMarksPage *self = (SysprofMarksPage *)page;
+  SysprofMarksPagePrivate *priv = sysprof_marks_page_get_instance_private (self);
+
+  g_assert (SYSPROF_IS_MARKS_PAGE (self));
+  g_assert (!hadjustment || GTK_IS_ADJUSTMENT (hadjustment));
+
+  gtk_scrolled_window_set_hadjustment (priv->scroller, hadjustment);
+}
+
+static void
+sysprof_marks_page_set_size_group (SysprofPage  *page,
+                                   GtkSizeGroup *size_group)
+{
+  SysprofMarksPage *self = (SysprofMarksPage *)page;
+  SysprofMarksPagePrivate *priv = sysprof_marks_page_get_instance_private (self);
+
+  g_assert (SYSPROF_IS_MARKS_PAGE (self));
+  g_assert (GTK_IS_SIZE_GROUP (size_group));
+
+  gtk_size_group_add_widget (size_group, GTK_WIDGET (priv->details_box));
+}
+
+static void
+sysprof_marks_page_finalize (GObject *object)
+{
+  SysprofMarksPage *self = (SysprofMarksPage *)object;
+  SysprofMarksPagePrivate *priv = sysprof_marks_page_get_instance_private (self);
 
   g_clear_object (&priv->zoom_manager);
 
-  G_OBJECT_CLASS (sysprof_marks_view_parent_class)->finalize (object);
+  G_OBJECT_CLASS (sysprof_marks_page_parent_class)->finalize (object);
 }
 
 static void
-sysprof_marks_view_get_property (GObject    *object,
+sysprof_marks_page_get_property (GObject    *object,
                                  guint       prop_id,
                                  GValue     *value,
                                  GParamSpec *pspec)
 {
-  SysprofMarksView *self = SYSPROF_MARKS_VIEW (object);
-  SysprofMarksViewPrivate *priv = sysprof_marks_view_get_instance_private (self);
+  SysprofMarksPage *self = SYSPROF_MARKS_PAGE (object);
+  SysprofMarksPagePrivate *priv = sysprof_marks_page_get_instance_private (self);
 
   switch (prop_id)
     {
@@ -264,13 +410,13 @@ sysprof_marks_view_get_property (GObject    *object,
 }
 
 static void
-sysprof_marks_view_set_property (GObject      *object,
+sysprof_marks_page_set_property (GObject      *object,
                                  guint         prop_id,
                                  const GValue *value,
                                  GParamSpec   *pspec)
 {
-  SysprofMarksView *self = SYSPROF_MARKS_VIEW (object);
-  SysprofMarksViewPrivate *priv = sysprof_marks_view_get_instance_private (self);
+  SysprofMarksPage *self = SYSPROF_MARKS_PAGE (object);
+  SysprofMarksPagePrivate *priv = sysprof_marks_page_get_instance_private (self);
 
   switch (prop_id)
     {
@@ -299,21 +445,33 @@ sysprof_marks_view_set_property (GObject      *object,
 }
 
 static void
-sysprof_marks_view_class_init (SysprofMarksViewClass *klass)
+sysprof_marks_page_class_init (SysprofMarksPageClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
-
-  object_class->finalize = sysprof_marks_view_finalize;
-  object_class->get_property = sysprof_marks_view_get_property;
-  object_class->set_property = sysprof_marks_view_set_property;
-
-  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/sysprof/ui/sysprof-marks-view.ui");
-  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksView, scroller);
-  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksView, tree_view);
-  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksView, duration_cell);
-  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksView, duration_column);
-  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksView, stack);
+  SysprofPageClass *page_class = SYSPROF_PAGE_CLASS (klass);
+
+  object_class->finalize = sysprof_marks_page_finalize;
+  object_class->get_property = sysprof_marks_page_get_property;
+  object_class->set_property = sysprof_marks_page_set_property;
+
+  page_class->load_async = sysprof_marks_page_load_async;
+  page_class->load_finish = sysprof_marks_page_load_finish;
+  page_class->set_hadjustment = sysprof_marks_page_set_hadjustment;
+  page_class->set_size_group = sysprof_marks_page_set_size_group;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/sysprof/ui/sysprof-marks-page.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksPage, details_box);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksPage, duration_cell);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksPage, duration_column);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksPage, scroller);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksPage, stack);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksPage, tree_view);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksPage, group);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksPage, mark);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksPage, duration);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksPage, time);
+  gtk_widget_class_bind_template_child_private (widget_class, SysprofMarksPage, message);
 
   properties [PROP_KIND] =
     g_param_spec_enum ("kind", NULL, NULL,
@@ -332,9 +490,9 @@ sysprof_marks_view_class_init (SysprofMarksViewClass *klass)
 }
 
 static void
-sysprof_marks_view_init (SysprofMarksView *self)
+sysprof_marks_page_init (SysprofMarksPage *self)
 {
-  SysprofMarksViewPrivate *priv = sysprof_marks_view_get_instance_private (self);
+  SysprofMarksPagePrivate *priv = sysprof_marks_page_get_instance_private (self);
 
   priv->kind = SYSPROF_MARKS_MODEL_MARKS;
 
@@ -345,123 +503,48 @@ sysprof_marks_view_init (SysprofMarksView *self)
 
   g_signal_connect_object (priv->tree_view,
                            "key-press-event",
-                           G_CALLBACK (sysprof_marks_view_tree_view_key_press_event_cb),
+                           G_CALLBACK (sysprof_marks_page_tree_view_key_press_event_cb),
                            self,
                            G_CONNECT_SWAPPED);
 
   g_signal_connect_object (priv->tree_view,
                            "query-tooltip",
-                           G_CALLBACK (sysprof_marks_view_tree_view_query_tooltip_cb),
+                           G_CALLBACK (sysprof_marks_page_tree_view_query_tooltip_cb),
                            self,
                            G_CONNECT_SWAPPED);
 
   g_signal_connect_object (gtk_tree_view_get_selection (priv->tree_view),
                            "changed",
-                           G_CALLBACK (sysprof_marks_view_selection_changed_cb),
+                           G_CALLBACK (sysprof_marks_page_selection_changed_cb),
                            self,
                            G_CONNECT_SWAPPED);
 }
 
 GtkWidget *
-sysprof_marks_view_new (void)
-{
-  return g_object_new (SYSPROF_TYPE_MARKS_VIEW, NULL);
-}
-
-static void
-sysprof_marks_view_load_cb (GObject      *object,
-                            GAsyncResult *result,
-                            gpointer      user_data)
-{
-  g_autoptr(SysprofMarksModel) model = NULL;
-  g_autoptr(GError) error = NULL;
-  g_autoptr(GTask) task = user_data;
-  SysprofMarksViewPrivate *priv;
-  SysprofCaptureReader *reader;
-  SysprofMarksView *self;
-
-  g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  priv = sysprof_marks_view_get_instance_private (self);
-
-  if (!(model = sysprof_marks_model_new_finish (result, &error)))
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-
-  reader = g_task_get_task_data (task);
-  g_assert (reader != NULL);
-
-  priv->capture_begin_time = sysprof_capture_reader_get_start_time (reader);
-  priv->capture_end_time = sysprof_capture_reader_get_end_time (reader);
-
-  g_object_set (priv->duration_cell,
-                "capture-begin-time", priv->capture_begin_time,
-                "capture-end-time", priv->capture_end_time,
-                "zoom-manager", priv->zoom_manager,
-                NULL);
-
-  gtk_tree_view_set_model (priv->tree_view, GTK_TREE_MODEL (model));
-
-  if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (model), NULL) == 0)
-    gtk_stack_set_visible_child_name (priv->stack, "empty-state");
-  else
-    gtk_stack_set_visible_child_name (priv->stack, "marks");
-
-  g_task_return_boolean (task, TRUE);
-}
-
-void
-sysprof_marks_view_load_async (SysprofMarksView     *self,
-                               SysprofCaptureReader *reader,
-                               SysprofSelection     *selection,
-                               GCancellable         *cancellable,
-                               GAsyncReadyCallback   callback,
-                               gpointer              user_data)
+sysprof_marks_page_new (SysprofZoomManager    *zoom_manager,
+                        SysprofMarksModelKind  kind)
 {
-  SysprofMarksViewPrivate *priv = sysprof_marks_view_get_instance_private (self);
-  g_autoptr(GTask) task = NULL;
+  SysprofMarksPage *self;
+  SysprofMarksPagePrivate *priv;
 
-  g_return_if_fail (SYSPROF_IS_MARKS_VIEW (self));
-  g_return_if_fail (reader != NULL);
-  g_return_if_fail (!selection || SYSPROF_IS_SELECTION (selection));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_val_if_fail (SYSPROF_IS_ZOOM_MANAGER (zoom_manager), NULL);
 
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_task_set_source_tag (task, sysprof_marks_view_load_async);
-  g_task_set_task_data (task,
-                        sysprof_capture_reader_ref (reader),
-                        (GDestroyNotify) sysprof_capture_reader_unref);
+  self = g_object_new (SYSPROF_TYPE_MARKS_PAGE,
+                       "zoom-manager", zoom_manager,
+                       NULL);
+  priv = sysprof_marks_page_get_instance_private (self);
+  priv->kind = kind;
 
-  sysprof_marks_model_new_async (reader,
-                                 priv->kind,
-                                 selection,
-                                 cancellable,
-                                 sysprof_marks_view_load_cb,
-                                 g_steal_pointer (&task));
-}
-
-gboolean
-sysprof_marks_view_load_finish (SysprofMarksView  *self,
-                                GAsyncResult      *result,
-                                GError           **error)
-{
-  g_return_val_if_fail (SYSPROF_IS_MARKS_VIEW (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
+  return GTK_WIDGET (self);
 }
 
 void
-_sysprof_marks_view_set_hadjustment (SysprofMarksView *self,
+_sysprof_marks_page_set_hadjustment (SysprofMarksPage *self,
                                      GtkAdjustment    *hadjustment)
 {
-  SysprofMarksViewPrivate *priv = sysprof_marks_view_get_instance_private (self);
+  SysprofMarksPagePrivate *priv = sysprof_marks_page_get_instance_private (self);
 
-  g_return_if_fail (SYSPROF_IS_MARKS_VIEW (self));
+  g_return_if_fail (SYSPROF_IS_MARKS_PAGE (self));
   g_return_if_fail (GTK_IS_ADJUSTMENT (hadjustment));
 
   gtk_scrolled_window_set_hadjustment (priv->scroller, hadjustment);
diff --git a/src/libsysprof-ui/sysprof-cpu-visualizer-row.h b/src/libsysprof-ui/sysprof-marks-page.h
similarity index 55%
rename from src/libsysprof-ui/sysprof-cpu-visualizer-row.h
rename to src/libsysprof-ui/sysprof-marks-page.h
index c8293c4..5070b1a 100644
--- a/src/libsysprof-ui/sysprof-cpu-visualizer-row.h
+++ b/src/libsysprof-ui/sysprof-marks-page.h
@@ -1,6 +1,6 @@
-/* sysprof-cpu-visualizer-row.h
+/* sysprof-marks-page.h
  *
- * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,22 +20,25 @@
 
 #pragma once
 
-#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
-# error "Only <sysprof-ui.h> can be included directly."
-#endif
+#include "sysprof-marks-model.h"
+#include "sysprof-page.h"
+#include "sysprof-zoom-manager.h"
 
-#include "sysprof-line-visualizer-row.h"
+G_BEGIN_DECLS
 
-#include "sysprof-version-macros.h"
+#define SYSPROF_TYPE_MARKS_PAGE (sysprof_marks_page_get_type())
 
-G_BEGIN_DECLS
+G_DECLARE_DERIVABLE_TYPE (SysprofMarksPage, sysprof_marks_page, SYSPROF, MARKS_PAGE, SysprofPage)
 
-#define SYSPROF_TYPE_CPU_VISUALIZER_ROW (sysprof_cpu_visualizer_row_get_type())
+struct _SysprofMarksPageClass
+{
+  SysprofPageClass parent_class;
 
-SYSPROF_AVAILABLE_IN_ALL
-G_DECLARE_FINAL_TYPE (SysprofCpuVisualizerRow, sysprof_cpu_visualizer_row, SYSPROF, CPU_VISUALIZER_ROW, 
SysprofLineVisualizerRow)
+  /*< private >*/
+  gpointer _reserved[16];
+};
 
-SYSPROF_AVAILABLE_IN_ALL
-GtkWidget *sysprof_cpu_visualizer_row_new (void);
+GtkWidget *sysprof_marks_page_new (SysprofZoomManager    *zoom_manager,
+                                   SysprofMarksModelKind  kind);
 
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-marks-page.ui b/src/libsysprof-ui/sysprof-marks-page.ui
new file mode 100644
index 0000000..a0a8491
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-marks-page.ui
@@ -0,0 +1,231 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="SysprofMarksPage" parent="SysprofPage">
+    <child>
+      <object class="GtkStack" id="stack">
+        <property name="homogeneous">false</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkBox" id="details_box">
+                <property name="orientation">vertical</property>
+                <property name="margin">6</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">Details</property>
+                    <property name="xalign">0</property>
+                    <property name="visible">true</property>
+                    <property name="margin-bottom">6</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkGrid">
+                    <property name="hexpand">false</property>
+                    <property name="vexpand">true</property>
+                    <property name="margin-start">6</property>
+                    <property name="visible">true</property>
+                    <property name="column-spacing">6</property>
+                    <property name="row-spacing">3</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">Group</property>
+                        <property name="visible">true</property>
+                        <property name="xalign">1</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="top-attach">0</property>
+                        <property name="left-attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">Mark</property>
+                        <property name="visible">true</property>
+                        <property name="xalign">1</property>
+                        <property name="yalign">0</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="top-attach">1</property>
+                        <property name="left-attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">Time</property>
+                        <property name="visible">true</property>
+                        <property name="xalign">1</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="top-attach">2</property>
+                        <property name="left-attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">Duration</property>
+                        <property name="visible">true</property>
+                        <property name="xalign">1</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="top-attach">3</property>
+                        <property name="left-attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">Message</property>
+                        <property name="visible">true</property>
+                        <property name="xalign">1</property>
+                        <property name="yalign">0</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="top-attach">4</property>
+                        <property name="left-attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="group">
+                        <property name="visible">true</property>
+                        <property name="xalign">0</property>
+                        <property name="wrap">true</property>
+                      </object>
+                      <packing>
+                        <property name="top-attach">0</property>
+                        <property name="left-attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="mark">
+                        <property name="visible">true</property>
+                        <property name="xalign">0</property>
+                        <property name="wrap">true</property>
+                      </object>
+                      <packing>
+                        <property name="top-attach">1</property>
+                        <property name="left-attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="time">
+                        <property name="visible">true</property>
+                        <property name="xalign">0</property>
+                      </object>
+                      <packing>
+                        <property name="top-attach">2</property>
+                        <property name="left-attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="duration">
+                        <property name="visible">true</property>
+                        <property name="xalign">0</property>
+                      </object>
+                      <packing>
+                        <property name="top-attach">3</property>
+                        <property name="left-attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkScrolledWindow">
+                        <property name="hexpand">true</property>
+                        <property name="shadow-type">in</property>
+                        <property name="visible">true</property>
+                        <child>
+                          <object class="GtkTextView" id="message">
+                            <property name="editable">false</property>
+                            <property name="visible">true</property>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="top-attach">4</property>
+                        <property name="left-attach">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkSeparator">
+                <property name="orientation">vertical</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="scroller">
+                <property name="hscrollbar-policy">external</property>
+                <property name="hexpand">true</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkTreeView" id="tree_view">
+                    <property name="headers-visible">false</property>
+                    <property name="enable-grid-lines">horizontal</property>
+                    <property name="has-tooltip">true</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkTreeViewColumn" id="duration_column">
+                        <property name="title" translatable="yes">Duration</property>
+                        <property name="expand">true</property>
+                        <child>
+                          <object class="SysprofCellRendererDuration" id="duration_cell">
+                            <property name="xalign">0</property>
+                            <property name="ypad">1</property>
+                          </object>
+                          <attributes>
+                            <attribute name="text">5</attribute>
+                            <attribute name="begin-time">2</attribute>
+                            <attribute name="end-time">3</attribute>
+                          </attributes>
+                          <cell-packing>
+                            <property name="expand">true</property>
+                          </cell-packing>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="name">marks</property>
+          </packing>
+        </child>
+        <child>
+          <object class="DzlEmptyState">
+            <property name="icon-name">computer-fail-symbolic</property>
+            <property name="title" translatable="yes">No Timings Available</property>
+            <property name="subtitle" translatable="yes">No timing data was found for the current 
selection</property>
+            <property name="visible">true</property>
+          </object>
+          <packing>
+            <property name="name">empty-state</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libsysprof-ui/sysprof-memory-aid.h b/src/libsysprof-ui/sysprof-memory-aid.h
index 424bd65..d33a1d8 100644
--- a/src/libsysprof-ui/sysprof-memory-aid.h
+++ b/src/libsysprof-ui/sysprof-memory-aid.h
@@ -26,10 +26,8 @@ G_BEGIN_DECLS
 
 #define SYSPROF_TYPE_MEMORY_AID (sysprof_memory_aid_get_type())
 
-SYSPROF_AVAILABLE_IN_ALL
 G_DECLARE_FINAL_TYPE (SysprofMemoryAid, sysprof_memory_aid, SYSPROF, MEMORY_AID, SysprofAid)
 
-SYSPROF_AVAILABLE_IN_ALL
 SysprofAid *sysprof_memory_aid_new (void);
 
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-model-filter.h b/src/libsysprof-ui/sysprof-model-filter.h
index ddbc2b9..a1b97bd 100644
--- a/src/libsysprof-ui/sysprof-model-filter.h
+++ b/src/libsysprof-ui/sysprof-model-filter.h
@@ -33,7 +33,7 @@ G_BEGIN_DECLS
 #define SYSPROF_TYPE_MODEL_FILTER (sysprof_model_filter_get_type())
 
 typedef gboolean (*SysprofModelFilterFunc) (GObject  *object,
-                                       gpointer  user_data);
+                                            gpointer  user_data);
 
 SYSPROF_AVAILABLE_IN_ALL
 G_DECLARE_DERIVABLE_TYPE (SysprofModelFilter, sysprof_model_filter, SYSPROF, MODEL_FILTER, GObject)
diff --git a/src/libsysprof-ui/sysprof-notebook.h b/src/libsysprof-ui/sysprof-notebook.h
index 2af9d86..676f4f5 100644
--- a/src/libsysprof-ui/sysprof-notebook.h
+++ b/src/libsysprof-ui/sysprof-notebook.h
@@ -20,6 +20,10 @@
 
 #pragma once
 
+#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
+# error "Only <sysprof-ui.h> can be included directly."
+#endif
+
 #include <gtk/gtk.h>
 #include <sysprof.h>
 
diff --git a/src/libsysprof-ui/sysprof-page.c b/src/libsysprof-ui/sysprof-page.c
new file mode 100644
index 0000000..af5f927
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-page.c
@@ -0,0 +1,235 @@
+/* sysprof-page.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-page"
+
+#include "config.h"
+
+#include "sysprof-page.h"
+
+typedef struct
+{
+  gchar *title;
+} SysprofPagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (SysprofPage, sysprof_page, GTK_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_TITLE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * sysprof_page_new:
+ *
+ * Create a new #SysprofPage.
+ *
+ * Returns: (transfer full) (type SysprofPage): a newly created #SysprofPage
+ */
+GtkWidget *
+sysprof_page_new (void)
+{
+  return g_object_new (SYSPROF_TYPE_PAGE, NULL);
+}
+
+const gchar *
+sysprof_page_get_title (SysprofPage *self)
+{
+  SysprofPagePrivate *priv = sysprof_page_get_instance_private (self);
+
+  g_return_val_if_fail (SYSPROF_IS_PAGE (self), NULL);
+
+  return priv->title;
+}
+
+void
+sysprof_page_set_title (SysprofPage *self,
+                        const gchar *title)
+{
+  SysprofPagePrivate *priv = sysprof_page_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_PAGE (self));
+
+  if (g_strcmp0 (priv->title, title) != 0)
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+}
+
+static void
+sysprof_page_real_load_async (SysprofPage             *self,
+                              SysprofCaptureReader    *reader,
+                              SysprofSelection        *selection,
+                              SysprofCaptureCondition *condition,
+                              GCancellable            *cancellable,
+                              GAsyncReadyCallback      callback,
+                              gpointer                 user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           sysprof_page_load_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Operation not supported");
+}
+
+static gboolean
+sysprof_page_real_load_finish (SysprofPage   *self,
+                               GAsyncResult  *result,
+                               GError       **error)
+{
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+sysprof_page_finalize (GObject *object)
+{
+  SysprofPage *self = (SysprofPage *)object;
+  SysprofPagePrivate *priv = sysprof_page_get_instance_private (self);
+
+  g_clear_pointer (&priv->title, g_free);
+
+  G_OBJECT_CLASS (sysprof_page_parent_class)->finalize (object);
+}
+
+static void
+sysprof_page_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  SysprofPage *self = SYSPROF_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_TITLE:
+      g_value_set_string (value, sysprof_page_get_title (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+sysprof_page_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  SysprofPage *self = SYSPROF_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_TITLE:
+      sysprof_page_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+sysprof_page_class_init (SysprofPageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = sysprof_page_finalize;
+  object_class->get_property = sysprof_page_get_property;
+  object_class->set_property = sysprof_page_set_property;
+
+  klass->load_async = sysprof_page_real_load_async;
+  klass->load_finish = sysprof_page_real_load_finish;
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title for the page",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+sysprof_page_init (SysprofPage *self)
+{
+}
+
+/**
+ * sysprof_page_load_async:
+ * @condition: (nullable): a #sysprofCaptureCondition or %NULL
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ *
+ * Since: 3.34
+ */
+void
+sysprof_page_load_async (SysprofPage             *self,
+                         SysprofCaptureReader    *reader,
+                         SysprofSelection        *selection,
+                         SysprofCaptureCondition *condition,
+                         GCancellable            *cancellable,
+                         GAsyncReadyCallback      callback,
+                         gpointer                 user_data)
+{
+  g_return_if_fail (SYSPROF_IS_PAGE (self));
+  g_return_if_fail (reader != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  SYSPROF_PAGE_GET_CLASS (self)->load_async (self, reader, selection, condition, cancellable, callback, 
user_data);
+}
+
+gboolean
+sysprof_page_load_finish (SysprofPage   *self,
+                          GAsyncResult  *result,
+                          GError       **error)
+{
+  g_return_val_if_fail (SYSPROF_IS_PAGE (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return SYSPROF_PAGE_GET_CLASS (self)->load_finish (self, result, error);
+}
+
+void
+sysprof_page_set_size_group (SysprofPage  *self,
+                             GtkSizeGroup *size_group)
+{
+  g_return_if_fail (SYSPROF_IS_PAGE (self));
+  g_return_if_fail (!size_group || GTK_IS_SIZE_GROUP (size_group));
+
+  if (SYSPROF_PAGE_GET_CLASS (self)->set_size_group)
+    SYSPROF_PAGE_GET_CLASS (self)->set_size_group (self, size_group);
+}
+
+void
+sysprof_page_set_hadjustment (SysprofPage   *self,
+                              GtkAdjustment *hadjustment)
+{
+  g_return_if_fail (SYSPROF_IS_PAGE (self));
+  g_return_if_fail (!hadjustment || GTK_IS_ADJUSTMENT (hadjustment));
+
+  if (SYSPROF_PAGE_GET_CLASS (self)->set_hadjustment)
+    SYSPROF_PAGE_GET_CLASS (self)->set_hadjustment (self, hadjustment);
+}
diff --git a/src/libsysprof-ui/sysprof-page.h b/src/libsysprof-ui/sysprof-page.h
new file mode 100644
index 0000000..783d998
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-page.h
@@ -0,0 +1,86 @@
+/* sysprof-page.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
+# error "Only <sysprof-ui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <sysprof.h>
+
+G_BEGIN_DECLS
+
+#define SYSPROF_TYPE_PAGE (sysprof_page_get_type())
+
+SYSPROF_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (SysprofPage, sysprof_page, SYSPROF, PAGE, GtkBin)
+
+struct _SysprofPageClass
+{
+  GtkBinClass parent_class;
+
+  void     (*load_async)      (SysprofPage              *self,
+                               SysprofCaptureReader     *reader,
+                               SysprofSelection         *selection,
+                               SysprofCaptureCondition  *condition,
+                               GCancellable             *cancellable,
+                               GAsyncReadyCallback       callback,
+                               gpointer                  user_data);
+  gboolean (*load_finish)     (SysprofPage              *self,
+                               GAsyncResult             *result,
+                               GError                  **error);
+  void     (*set_hadjustment) (SysprofPage              *self,
+                               GtkAdjustment            *hadjustment);
+  void     (*set_size_group)  (SysprofPage              *self,
+                               GtkSizeGroup             *size_group);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+SYSPROF_AVAILABLE_IN_ALL
+GtkWidget   *sysprof_page_new             (void);
+SYSPROF_AVAILABLE_IN_ALL
+void         sysprof_page_load_async      (SysprofPage              *self,
+                                           SysprofCaptureReader     *reader,
+                                           SysprofSelection         *selection,
+                                           SysprofCaptureCondition  *condition,
+                                           GCancellable             *cancellable,
+                                           GAsyncReadyCallback       callback,
+                                           gpointer                  user_data);
+SYSPROF_AVAILABLE_IN_ALL
+gboolean     sysprof_page_load_finish     (SysprofPage              *self,
+                                           GAsyncResult             *result,
+                                           GError                  **error);
+SYSPROF_AVAILABLE_IN_ALL
+const gchar *sysprof_page_get_title       (SysprofPage              *self);
+SYSPROF_AVAILABLE_IN_ALL
+void         sysprof_page_set_title       (SysprofPage              *self,
+                                           const gchar              *title);
+SYSPROF_AVAILABLE_IN_ALL
+void         sysprof_page_set_hadjustment (SysprofPage              *self,
+                                           GtkAdjustment            *hadjustment);
+SYSPROF_AVAILABLE_IN_ALL
+void         sysprof_page_set_size_group  (SysprofPage              *self,
+                                           GtkSizeGroup             *size_group);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-profiler-assistant.c b/src/libsysprof-ui/sysprof-profiler-assistant.c
index e3db2bf..a8a6622 100644
--- a/src/libsysprof-ui/sysprof-profiler-assistant.c
+++ b/src/libsysprof-ui/sysprof-profiler-assistant.c
@@ -27,13 +27,17 @@
 #include "sysprof-platform.h"
 
 #include "sysprof-aid-icon.h"
-#include "sysprof-cpu-aid.h"
 #include "sysprof-environ-editor.h"
 #include "sysprof-profiler-assistant.h"
-#include "sysprof-proxy-aid.h"
 #include "sysprof-process-model-row.h"
 #include "sysprof-ui-private.h"
 
+#include "sysprof-battery-aid.h"
+#include "sysprof-callgraph-aid.h"
+#include "sysprof-cpu-aid.h"
+#include "sysprof-memory-aid.h"
+#include "sysprof-proxy-aid.h"
+
 struct _SysprofProfilerAssistant
 {
   GtkBin                parent_instance;
@@ -298,9 +302,12 @@ sysprof_profiler_assistant_class_init (SysprofProfilerAssistantClass *klass)
   gtk_widget_class_bind_template_child (widget_class, SysprofProfilerAssistant, inherit_switch);
 
   g_type_ensure (SYSPROF_TYPE_AID_ICON);
+  g_type_ensure (SYSPROF_TYPE_BATTERY_AID);
   g_type_ensure (SYSPROF_TYPE_CPU_AID);
+  g_type_ensure (SYSPROF_TYPE_MEMORY_AID);
   g_type_ensure (SYSPROF_TYPE_PROXY_AID);
   g_type_ensure (SYSPROF_TYPE_ENVIRON_EDITOR);
+  g_type_ensure (SYSPROF_TYPE_CALLGRAPH_AID);
 }
 
 static void
diff --git a/src/libsysprof-ui/sysprof-profiler-assistant.h b/src/libsysprof-ui/sysprof-profiler-assistant.h
index 8bc6df5..f57498a 100644
--- a/src/libsysprof-ui/sysprof-profiler-assistant.h
+++ b/src/libsysprof-ui/sysprof-profiler-assistant.h
@@ -28,10 +28,8 @@ G_BEGIN_DECLS
 
 #define SYSPROF_TYPE_PROFILER_ASSISTANT (sysprof_profiler_assistant_get_type())
 
-SYSPROF_AVAILABLE_IN_ALL
 G_DECLARE_FINAL_TYPE (SysprofProfilerAssistant, sysprof_profiler_assistant, SYSPROF, PROFILER_ASSISTANT, 
GtkBin)
 
-SYSPROF_AVAILABLE_IN_ALL
 GtkWidget *sysprof_profiler_assistant_new (void);
 
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-profiler-assistant.ui b/src/libsysprof-ui/sysprof-profiler-assistant.ui
index 7a08003..2a15f30 100644
--- a/src/libsysprof-ui/sysprof-profiler-assistant.ui
+++ b/src/libsysprof-ui/sysprof-profiler-assistant.ui
@@ -43,13 +43,7 @@
       </object>
     </child>
   </object>
-  <object class="SysprofAid" id="battery_aid">
-    <property name="display-name">Battery</property>
-    <property name="icon-name">battery-low-charging-symbolic</property>
-    <child>
-      <object class="SysprofBatterySource"/>
-    </child>
-  </object>
+  <object class="SysprofBatteryAid" id="battery_aid"/>
   <template class="SysprofProfilerAssistant" parent="GtkBin">
     <child>
       <object class="GtkScrolledWindow">
diff --git a/src/libsysprof-ui/sysprof-proxy-aid.h b/src/libsysprof-ui/sysprof-proxy-aid.h
index 4f6a79d..218ba11 100644
--- a/src/libsysprof-ui/sysprof-proxy-aid.h
+++ b/src/libsysprof-ui/sysprof-proxy-aid.h
@@ -36,13 +36,10 @@ struct _SysprofProxyAidClass
   gpointer _reserved[8];
 };
 
-SYSPROF_AVAILABLE_IN_ALL
 void sysprof_proxy_aid_set_bus_type    (SysprofProxyAid *self,
                                         GBusType         bus_type);
-SYSPROF_AVAILABLE_IN_ALL
 void sysprof_proxy_aid_set_bus_name    (SysprofProxyAid *self,
                                         const gchar     *bus_name);
-SYSPROF_AVAILABLE_IN_ALL
 void sysprof_proxy_aid_set_object_path (SysprofProxyAid *self,
                                         const gchar     *obj_path);
 
diff --git a/src/libsysprof-ui/sysprof-recording-state-view.h 
b/src/libsysprof-ui/sysprof-recording-state-view.h
index 82d3b8e..ffc149c 100644
--- a/src/libsysprof-ui/sysprof-recording-state-view.h
+++ b/src/libsysprof-ui/sysprof-recording-state-view.h
@@ -20,10 +20,6 @@
 
 #pragma once
 
-#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
-# error "Only <sysprof-ui.h> can be included directly."
-#endif
-
 #include <gtk/gtk.h>
 #include <sysprof.h>
 
@@ -31,7 +27,6 @@ G_BEGIN_DECLS
 
 #define SYSPROF_TYPE_RECORDING_STATE_VIEW (sysprof_recording_state_view_get_type())
 
-SYSPROF_AVAILABLE_IN_ALL
 G_DECLARE_DERIVABLE_TYPE (SysprofRecordingStateView, sysprof_recording_state_view, SYSPROF, 
RECORDING_STATE_VIEW, GtkBin)
 
 struct _SysprofRecordingStateViewClass
@@ -41,9 +36,7 @@ struct _SysprofRecordingStateViewClass
   gpointer padding[4];
 };
 
-SYSPROF_AVAILABLE_IN_ALL
 GtkWidget *sysprof_recording_state_view_new          (void);
-SYSPROF_AVAILABLE_IN_ALL
 void       sysprof_recording_state_view_set_profiler (SysprofRecordingStateView *self,
                                                       SysprofProfiler           *profiler);
 
diff --git a/src/libsysprof-ui/sysprof-scrollmap.c b/src/libsysprof-ui/sysprof-scrollmap.c
new file mode 100644
index 0000000..41d4478
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-scrollmap.c
@@ -0,0 +1,306 @@
+/* sysprof-scrollmap.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-scrollmap"
+
+#include "config.h"
+
+#include "sysprof-scrollmap.h"
+
+#define BOX_SIZE 4
+
+struct _SysprofScrollmap
+{
+  GtkScrollbar  parent_instance;
+
+  gint64        begin_time;
+  gint64        end_time;
+
+  GArray       *timings;
+  GArray       *buckets;
+  GCancellable *cancellable;
+
+  gint          most;
+};
+
+typedef struct
+{
+  gint64  begin_time;
+  gint64  end_time;
+  GArray *timings;
+  gint    width;
+  gint    height;
+} Recalculate;
+
+G_DEFINE_TYPE (SysprofScrollmap, sysprof_scrollmap, GTK_TYPE_SCROLLBAR)
+
+static void
+recalculate_free (gpointer data)
+{
+  Recalculate *state = data;
+
+  g_clear_pointer (&state->timings, g_array_unref);
+  g_slice_free (Recalculate, state);
+}
+
+static void
+sysprof_scrollmap_recalculate_worker (GTask        *task,
+                                      gpointer      source_object,
+                                      gpointer      task_data,
+                                      GCancellable *cancellable)
+{
+  Recalculate *state = task_data;
+  g_autoptr(GArray) buckets = NULL;
+  gint64 duration;
+  gint n_buckets;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (SYSPROF_IS_SCROLLMAP (source_object));
+  g_assert (state != NULL);
+  g_assert (state->timings != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  duration = state->end_time - state->begin_time;
+  n_buckets = MAX (1, state->width / (BOX_SIZE + 1));
+  buckets = g_array_sized_new (FALSE, TRUE, sizeof (gint), n_buckets);
+  g_array_set_size (buckets, n_buckets);
+
+  for (guint i = 0; i < state->timings->len; i++)
+    {
+      gint64 t = g_array_index (state->timings, gint64, i);
+      gint n;
+
+      if (t < state->begin_time || t > state->end_time)
+        continue;
+
+      n = ((t - state->begin_time) / (gdouble)duration) * n_buckets;
+
+      g_assert (n < n_buckets);
+
+      g_array_index (buckets, gint, n)++;
+    }
+
+  g_task_return_pointer (task,
+                         g_steal_pointer (&buckets),
+                         (GDestroyNotify) g_array_unref);
+}
+
+static void
+sysprof_scrollmap_recalculate_async (SysprofScrollmap    *self,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  GtkAllocation alloc;
+  Recalculate state;
+
+  g_assert (SYSPROF_IS_SCROLLMAP (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_scrollmap_recalculate_async);
+
+  if (self->timings == NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_CANCELLED,
+                               "The operation was cancelled");
+      return;
+    }
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  state.begin_time = self->begin_time;
+  state.end_time = self->end_time;
+  state.width = alloc.width;
+  state.height = alloc.height;
+  state.timings = g_array_ref (self->timings);
+
+  g_task_set_task_data (task,
+                        g_slice_dup (Recalculate, &state),
+                        recalculate_free);
+  g_task_run_in_thread (task, sysprof_scrollmap_recalculate_worker);
+}
+
+static GArray *
+sysprof_scrollmap_recalculate_finish (SysprofScrollmap  *self,
+                                      GAsyncResult      *result,
+                                      GError           **error)
+{
+  g_assert (SYSPROF_IS_SCROLLMAP (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+draw_boxes (const GtkAllocation *alloc,
+            cairo_t             *cr,
+            gint                 x,
+            gint                 n_boxes)
+{
+  gint y;
+
+  g_assert (cr != NULL);
+
+  y = alloc->height - BOX_SIZE;
+
+  for (gint i = 0; i < n_boxes; i++)
+    {
+      cairo_rectangle (cr, x, y, BOX_SIZE, -BOX_SIZE);
+      y -= (BOX_SIZE + 1);
+    }
+
+  cairo_fill (cr);
+}
+
+static gboolean
+sysprof_scrollmap_draw (GtkWidget *widget,
+                        cairo_t   *cr)
+{
+  SysprofScrollmap *self = (SysprofScrollmap *)widget;
+  GtkStyleContext *style_context;
+  GtkAllocation alloc;
+  GdkRGBA color;
+  gint max_boxes;
+
+  g_assert (SYSPROF_IS_SCROLLMAP (self));
+  g_assert (cr != NULL);
+
+  if (self->buckets == NULL)
+    goto chainup;
+
+  gtk_widget_get_allocation (widget, &alloc);
+  max_boxes = alloc.height / (BOX_SIZE + 1) - 1;
+
+  style_context = gtk_widget_get_style_context (widget);
+  gtk_style_context_get_color (style_context,
+                               gtk_style_context_get_state (style_context),
+                               &color);
+  gdk_cairo_set_source_rgba (cr, &color);
+
+  for (guint i = 0; i < self->buckets->len; i++)
+    {
+      gint n = g_array_index (self->buckets, gint, i);
+      gint x = 1 + i * (BOX_SIZE + 1);
+      gint b = max_boxes * (n / (gdouble)self->most);
+
+#if 1
+      if (n > 0)
+        b = MAX (b, 1);
+#endif
+
+      draw_boxes (&alloc, cr, x, b);
+    }
+
+chainup:
+  return GTK_WIDGET_CLASS (sysprof_scrollmap_parent_class)->draw (widget, cr);
+}
+
+static void
+sysprof_scrollmap_recalculate_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  SysprofScrollmap *self = (SysprofScrollmap *)object;
+  g_autoptr(GArray) buckets = NULL;
+
+  g_assert (SYSPROF_IS_SCROLLMAP (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (user_data == NULL);
+
+  if ((buckets = sysprof_scrollmap_recalculate_finish (self, result, NULL)))
+    {
+      self->most = 0;
+
+      for (guint i = 0; i < buckets->len; i++)
+        {
+          gint n = g_array_index (buckets, gint, i);
+          self->most = MAX (self->most, n);
+        }
+
+      g_clear_pointer (&self->buckets, g_array_unref);
+      self->buckets = g_steal_pointer (&buckets);
+
+      gtk_widget_queue_draw (GTK_WIDGET (self));
+    }
+}
+
+static void
+sysprof_scrollmap_finalize (GObject *object)
+{
+  SysprofScrollmap *self = (SysprofScrollmap *)object;
+
+  g_clear_pointer (&self->buckets, g_array_unref);
+  g_clear_pointer (&self->timings, g_array_unref);
+
+  G_OBJECT_CLASS (sysprof_scrollmap_parent_class)->finalize (object);
+}
+
+static void
+sysprof_scrollmap_class_init (SysprofScrollmapClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = sysprof_scrollmap_finalize;
+
+  widget_class->draw = sysprof_scrollmap_draw;
+}
+
+static void
+sysprof_scrollmap_init (SysprofScrollmap *self)
+{
+}
+
+void
+sysprof_scrollmap_set_timings (SysprofScrollmap *self,
+                               GArray           *timings)
+{
+  g_return_if_fail (SYSPROF_IS_SCROLLMAP (self));
+
+  if (timings != self->timings)
+    {
+      g_clear_pointer (&self->timings, g_array_unref);
+      self->timings = timings ? g_array_ref (timings) : NULL;
+    }
+}
+
+void
+sysprof_scrollmap_set_time_range (SysprofScrollmap *self,
+                                  gint64            begin_time,
+                                  gint64            end_time)
+{
+  g_return_if_fail (SYSPROF_IS_SCROLLMAP (self));
+
+  self->begin_time = begin_time;
+  self->end_time = end_time;
+
+  g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->cancellable);
+  self->cancellable = g_cancellable_new ();
+
+  sysprof_scrollmap_recalculate_async (self,
+                                       self->cancellable,
+                                       sysprof_scrollmap_recalculate_cb,
+                                       NULL);
+}
diff --git a/src/libsysprof-ui/sysprof-scrollmap.h b/src/libsysprof-ui/sysprof-scrollmap.h
new file mode 100644
index 0000000..e7884aa
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-scrollmap.h
@@ -0,0 +1,37 @@
+/* sysprof-scrollmap.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define SYSPROF_TYPE_SCROLLMAP (sysprof_scrollmap_get_type())
+
+G_DECLARE_FINAL_TYPE (SysprofScrollmap, sysprof_scrollmap, SYSPROF, SCROLLMAP, GtkScrollbar)
+
+void sysprof_scrollmap_set_timings    (SysprofScrollmap *self,
+                                       GArray           *timings);
+void sysprof_scrollmap_set_time_range (SysprofScrollmap *self,
+                                       gint64            begin_time,
+                                       gint64            end_time);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-theme-manager.c b/src/libsysprof-ui/sysprof-theme-manager.c
index b1ebfe5..1094893 100644
--- a/src/libsysprof-ui/sysprof-theme-manager.c
+++ b/src/libsysprof-ui/sysprof-theme-manager.c
@@ -202,9 +202,9 @@ sysprof_theme_manager_get_default (void)
 
 guint
 sysprof_theme_manager_register_resource (SysprofThemeManager *self,
-                                    const gchar    *theme_name,
-                                    const gchar    *variant,
-                                    const gchar    *resource)
+                                         const gchar         *theme_name,
+                                         const gchar         *variant,
+                                         const gchar         *resource)
 {
   ThemeResource *theme_resource;
   static guint counter;
diff --git a/src/libsysprof-ui/sysprof-time-visualizer.c b/src/libsysprof-ui/sysprof-time-visualizer.c
new file mode 100644
index 0000000..cb1d01a
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-time-visualizer.c
@@ -0,0 +1,543 @@
+/* sysprof-time-visualizer.c
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-time-visualizer"
+
+#include "config.h"
+
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysprof.h>
+
+#include "pointcache.h"
+#include "sysprof-time-visualizer.h"
+
+typedef struct
+{
+  /*
+   * Our reader as assigned by the visualizer system.
+   */
+  SysprofCaptureReader *reader;
+
+  /*
+   * An array of LineInfo which contains information about the counters
+   * we need to render.
+   */
+  GArray *lines;
+
+  /*
+   * This is our set of cached points to render. Once it is assigned here,
+   * it is immutable (and therefore may be shared with worker processes
+   * that are rendering the points).
+   */
+  PointCache *cache;
+
+  /*
+   * If we have a new counter discovered or the reader is set, we might
+   * want to delay loading until we return to the main loop. This can
+   * help us avoid doing duplicate work.
+   */
+  guint queued_load;
+} SysprofTimeVisualizerPrivate;
+
+typedef struct
+{
+  guint id;
+  gdouble line_width;
+  GdkRGBA rgba;
+  guint use_default_style : 1;
+  guint use_dash : 1;
+} LineInfo;
+
+typedef struct
+{
+  SysprofCaptureCursor *cursor;
+  GArray *lines;
+  PointCache *cache;
+  gint64 begin_time;
+  gint64 end_time;
+} LoadData;
+
+G_DEFINE_TYPE_WITH_PRIVATE (SysprofTimeVisualizer, sysprof_time_visualizer, SYSPROF_TYPE_VISUALIZER)
+
+static void        sysprof_time_visualizer_load_data_async  (SysprofTimeVisualizer  *self,
+                                                             GCancellable           *cancellable,
+                                                             GAsyncReadyCallback     callback,
+                                                             gpointer                user_data);
+static PointCache *sysprof_time_visualizer_load_data_finish (SysprofTimeVisualizer  *self,
+                                                             GAsyncResult           *result,
+                                                             GError                **error);
+
+static gdouble dashes[] = { 1.0, 2.0 };
+
+static void
+load_data_free (gpointer data)
+{
+  LoadData *load = data;
+
+  if (load != NULL)
+    {
+      g_clear_pointer (&load->lines, g_array_unref);
+      g_clear_pointer (&load->cursor, sysprof_capture_cursor_unref);
+      g_clear_pointer (&load->cache, point_cache_unref);
+      g_slice_free (LoadData, load);
+    }
+}
+
+static GArray *
+copy_array (GArray *ar)
+{
+  GArray *ret;
+
+  ret = g_array_sized_new (FALSE, FALSE, g_array_get_element_size (ar), ar->len);
+  g_array_set_size (ret, ar->len);
+  memcpy (ret->data, ar->data, ar->len * g_array_get_element_size (ret));
+
+  return ret;
+}
+
+static gboolean
+sysprof_time_visualizer_draw (GtkWidget *widget,
+                              cairo_t   *cr)
+{
+  SysprofTimeVisualizer *self = (SysprofTimeVisualizer *)widget;
+  SysprofTimeVisualizerPrivate *priv = sysprof_time_visualizer_get_instance_private (self);
+  GtkStyleContext *style_context;
+  GtkStateFlags flags;
+  GtkAllocation alloc;
+  GdkRectangle clip;
+  GdkRGBA foreground;
+  gboolean ret;
+
+  g_assert (SYSPROF_IS_TIME_VISUALIZER (widget));
+  g_assert (cr != NULL);
+
+  gtk_widget_get_allocation (widget, &alloc);
+
+  ret = GTK_WIDGET_CLASS (sysprof_time_visualizer_parent_class)->draw (widget, cr);
+
+  if (priv->cache == NULL)
+    return ret;
+
+  if (!gdk_cairo_get_clip_rectangle (cr, &clip))
+    return ret;
+
+  style_context = gtk_widget_get_style_context (widget);
+  flags = gtk_widget_get_state_flags (widget);
+  gtk_style_context_get_color (style_context, flags, &foreground);
+
+  gdk_cairo_set_source_rgba (cr, &foreground);
+
+  for (guint line = 0; line < priv->lines->len; line++)
+    {
+      g_autofree SysprofVisualizerAbsolutePoint *points = NULL;
+      const LineInfo *line_info = &g_array_index (priv->lines, LineInfo, line);
+      const Point *fpoints;
+      guint n_fpoints = 0;
+
+      fpoints = point_cache_get_points (priv->cache, line_info->id, &n_fpoints);
+
+      if (n_fpoints > 0)
+        {
+          guint last_x = G_MAXUINT;
+
+          points = g_new0 (SysprofVisualizerAbsolutePoint, n_fpoints);
+
+          sysprof_visualizer_translate_points (SYSPROF_VISUALIZER (self),
+                                               (const SysprofVisualizerRelativePoint *)fpoints,
+                                               n_fpoints,
+                                               points,
+                                               n_fpoints);
+
+          cairo_set_line_width (cr, 1.0);
+
+          for (guint i = 0; i < n_fpoints; i++)
+            {
+              if ((guint)points[i].x != last_x)
+                last_x = (guint)points[i].x;
+              else
+                continue;
+
+              cairo_move_to (cr, (guint)points[i].x + .5, alloc.height / 3);
+              cairo_line_to (cr, (guint)points[i].x + .5, alloc.height / 3 * 2);
+            }
+
+          if (line_info->use_dash)
+            cairo_set_dash (cr, dashes, G_N_ELEMENTS (dashes), 0);
+
+          cairo_stroke (cr);
+        }
+    }
+
+  return ret;
+}
+
+static void
+sysprof_time_visualizer_load_data_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  SysprofTimeVisualizer *self = (SysprofTimeVisualizer *)object;
+  SysprofTimeVisualizerPrivate *priv = sysprof_time_visualizer_get_instance_private (self);
+  g_autoptr(GError) error = NULL;
+  g_autoptr(PointCache) cache = NULL;
+
+  g_assert (SYSPROF_IS_TIME_VISUALIZER (self));
+
+  cache = sysprof_time_visualizer_load_data_finish (self, result, &error);
+
+  if (cache == NULL)
+    {
+      g_warning ("%s", error->message);
+      return;
+    }
+
+  g_clear_pointer (&priv->cache, point_cache_unref);
+  priv->cache = g_steal_pointer (&cache);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static gboolean
+sysprof_time_visualizer_do_reload (gpointer data)
+{
+  SysprofTimeVisualizer *self = data;
+  SysprofTimeVisualizerPrivate *priv = sysprof_time_visualizer_get_instance_private (self);
+
+  g_assert (SYSPROF_IS_TIME_VISUALIZER (self));
+
+  priv->queued_load = 0;
+  if (priv->reader != NULL)
+    sysprof_time_visualizer_load_data_async (self,
+                                            NULL,
+                                            sysprof_time_visualizer_load_data_cb,
+                                            NULL);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+sysprof_time_visualizer_queue_reload (SysprofTimeVisualizer *self)
+{
+  SysprofTimeVisualizerPrivate *priv = sysprof_time_visualizer_get_instance_private (self);
+
+  g_assert (SYSPROF_IS_TIME_VISUALIZER (self));
+
+  if (priv->queued_load == 0)
+    priv->queued_load =
+      gdk_threads_add_idle_full (G_PRIORITY_LOW,
+                                 sysprof_time_visualizer_do_reload,
+                                 self,
+                                 NULL);
+}
+
+static void
+sysprof_time_visualizer_set_reader (SysprofVisualizer    *row,
+                                    SysprofCaptureReader *reader)
+{
+  SysprofTimeVisualizer *self = (SysprofTimeVisualizer *)row;
+  SysprofTimeVisualizerPrivate *priv = sysprof_time_visualizer_get_instance_private (self);
+
+  g_assert (SYSPROF_IS_TIME_VISUALIZER (self));
+
+  if (priv->reader != reader)
+    {
+      if (priv->reader != NULL)
+        {
+          sysprof_capture_reader_unref (priv->reader);
+          priv->reader = NULL;
+        }
+
+      if (reader != NULL)
+        priv->reader = sysprof_capture_reader_ref (reader);
+
+      sysprof_time_visualizer_queue_reload (self);
+    }
+}
+
+static void
+sysprof_time_visualizer_finalize (GObject *object)
+{
+  SysprofTimeVisualizer *self = (SysprofTimeVisualizer *)object;
+  SysprofTimeVisualizerPrivate *priv = sysprof_time_visualizer_get_instance_private (self);
+
+  g_clear_pointer (&priv->lines, g_array_unref);
+  g_clear_pointer (&priv->cache, point_cache_unref);
+  g_clear_pointer (&priv->reader, sysprof_capture_reader_unref);
+
+  if (priv->queued_load != 0)
+    {
+      g_source_remove (priv->queued_load);
+      priv->queued_load = 0;
+    }
+
+  G_OBJECT_CLASS (sysprof_time_visualizer_parent_class)->finalize (object);
+}
+
+static void
+sysprof_time_visualizer_class_init (SysprofTimeVisualizerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  SysprofVisualizerClass *visualizer_class = SYSPROF_VISUALIZER_CLASS (klass);
+
+  object_class->finalize = sysprof_time_visualizer_finalize;
+
+  widget_class->draw = sysprof_time_visualizer_draw;
+
+  visualizer_class->set_reader = sysprof_time_visualizer_set_reader;
+}
+
+static void
+sysprof_time_visualizer_init (SysprofTimeVisualizer *self)
+{
+  SysprofTimeVisualizerPrivate *priv = sysprof_time_visualizer_get_instance_private (self);
+
+  priv->lines = g_array_new (FALSE, FALSE, sizeof (LineInfo));
+}
+
+void
+sysprof_time_visualizer_add_counter (SysprofTimeVisualizer *self,
+                                     guint                     counter_id,
+                                     const GdkRGBA            *color)
+{
+  SysprofTimeVisualizerPrivate *priv = sysprof_time_visualizer_get_instance_private (self);
+  LineInfo line_info = {0};
+
+  g_assert (SYSPROF_IS_TIME_VISUALIZER (self));
+  g_assert (priv->lines != NULL);
+
+  line_info.id = counter_id;
+  line_info.line_width = 1.0;
+
+  if (color != NULL)
+    {
+      line_info.rgba = *color;
+      line_info.use_default_style = FALSE;
+    }
+  else
+    {
+      line_info.use_default_style = TRUE;
+    }
+
+  g_array_append_val (priv->lines, line_info);
+
+  if (SYSPROF_TIME_VISUALIZER_GET_CLASS (self)->counter_added)
+    SYSPROF_TIME_VISUALIZER_GET_CLASS (self)->counter_added (self, counter_id);
+
+  sysprof_time_visualizer_queue_reload (self);
+}
+
+void
+sysprof_time_visualizer_clear (SysprofTimeVisualizer *self)
+{
+  SysprofTimeVisualizerPrivate *priv = sysprof_time_visualizer_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_TIME_VISUALIZER (self));
+
+  if (priv->lines->len > 0)
+    g_array_remove_range (priv->lines, 0, priv->lines->len);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static inline gboolean
+contains_id (GArray *ar,
+             guint   id)
+{
+  for (guint i = 0; i < ar->len; i++)
+    {
+      const LineInfo *info = &g_array_index (ar, LineInfo, i);
+
+      if (info->id == id)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static inline gdouble
+calc_x (gint64 lower,
+        gint64 upper,
+        gint64 value)
+{
+  return (gdouble)(value - lower) / (gdouble)(upper - lower);
+}
+
+static gboolean
+sysprof_time_visualizer_load_data_frame_cb (const SysprofCaptureFrame *frame,
+                                            gpointer                   user_data)
+{
+  LoadData *load = user_data;
+
+  g_assert (frame != NULL);
+  g_assert (frame->type == SYSPROF_CAPTURE_FRAME_CTRSET ||
+            frame->type == SYSPROF_CAPTURE_FRAME_CTRDEF);
+  g_assert (load != NULL);
+
+  if (frame->type == SYSPROF_CAPTURE_FRAME_CTRSET)
+    {
+      const SysprofCaptureCounterSet *set = (SysprofCaptureCounterSet *)frame;
+      gdouble x = calc_x (load->begin_time, load->end_time, frame->time);
+
+      for (guint i = 0; i < set->n_values; i++)
+        {
+          const SysprofCaptureCounterValues *group = &set->values[i];
+
+          for (guint j = 0; j < G_N_ELEMENTS (group->ids); j++)
+            {
+              guint counter_id = group->ids[j];
+
+              if (counter_id != 0 && contains_id (load->lines, counter_id))
+                point_cache_add_point_to_set (load->cache, counter_id, x, 0);
+            }
+        }
+    }
+
+  return TRUE;
+}
+
+static void
+sysprof_time_visualizer_load_data_worker (GTask        *task,
+                                          gpointer      source_object,
+                                          gpointer      task_data,
+                                          GCancellable *cancellable)
+{
+  LoadData *load = task_data;
+  g_autoptr(GArray) counter_ids = NULL;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (SYSPROF_IS_TIME_VISUALIZER (source_object));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  counter_ids = g_array_new (FALSE, FALSE, sizeof (guint));
+
+  for (guint i = 0; i < load->lines->len; i++)
+    {
+      const LineInfo *line_info = &g_array_index (load->lines, LineInfo, i);
+      g_array_append_val (counter_ids, line_info->id);
+    }
+
+  sysprof_capture_cursor_add_condition (load->cursor,
+                                        sysprof_capture_condition_new_where_counter_in (counter_ids->len,
+                                                                                        (guint 
*)(gpointer)counter_ids->data));
+  sysprof_capture_cursor_foreach (load->cursor, sysprof_time_visualizer_load_data_frame_cb, load);
+  g_task_return_pointer (task, g_steal_pointer (&load->cache), (GDestroyNotify)point_cache_unref);
+}
+
+static void
+sysprof_time_visualizer_load_data_async (SysprofTimeVisualizer *self,
+                                         GCancellable          *cancellable,
+                                         GAsyncReadyCallback    callback,
+                                         gpointer               user_data)
+{
+  SysprofTimeVisualizerPrivate *priv = sysprof_time_visualizer_get_instance_private (self);
+  g_autoptr(GTask) task = NULL;
+  LoadData *load;
+
+  g_assert (SYSPROF_IS_TIME_VISUALIZER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_priority (task, G_PRIORITY_LOW);
+  g_task_set_source_tag (task, sysprof_time_visualizer_load_data_async);
+
+  if (priv->reader == NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_FAILED,
+                               "No data loaded");
+      return;
+    }
+
+  load = g_slice_new0 (LoadData);
+  load->cache = point_cache_new ();
+  load->begin_time = sysprof_capture_reader_get_start_time (priv->reader);
+  load->end_time = sysprof_capture_reader_get_end_time (priv->reader);
+  load->cursor = sysprof_capture_cursor_new (priv->reader);
+  load->lines = copy_array (priv->lines);
+
+  for (guint i = 0; i < load->lines->len; i++)
+    {
+      const LineInfo *line_info = &g_array_index (load->lines, LineInfo, i);
+
+      point_cache_add_set (load->cache, line_info->id);
+    }
+
+  g_task_set_task_data  (task, load, load_data_free);
+  g_task_run_in_thread (task, sysprof_time_visualizer_load_data_worker);
+}
+
+static PointCache *
+sysprof_time_visualizer_load_data_finish (SysprofTimeVisualizer  *self,
+                                          GAsyncResult           *result,
+                                          GError                **error)
+{
+  g_assert (SYSPROF_IS_TIME_VISUALIZER (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+void
+sysprof_time_visualizer_set_line_width (SysprofTimeVisualizer *self,
+                                        guint                  counter_id,
+                                        gdouble                width)
+{
+  SysprofTimeVisualizerPrivate *priv = sysprof_time_visualizer_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_TIME_VISUALIZER (self));
+
+  for (guint i = 0; i < priv->lines->len; i++)
+    {
+      LineInfo *info = &g_array_index (priv->lines, LineInfo, i);
+
+      if (info->id == counter_id)
+        {
+          info->line_width = width;
+          sysprof_time_visualizer_queue_reload (self);
+          break;
+        }
+    }
+}
+
+void
+sysprof_time_visualizer_set_dash (SysprofTimeVisualizer *self,
+                                  guint                  counter_id,
+                                  gboolean               use_dash)
+{
+  SysprofTimeVisualizerPrivate *priv = sysprof_time_visualizer_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_TIME_VISUALIZER (self));
+
+  for (guint i = 0; i < priv->lines->len; i++)
+    {
+      LineInfo *info = &g_array_index (priv->lines, LineInfo, i);
+
+      if (info->id == counter_id)
+        {
+          info->use_dash = !!use_dash;
+          sysprof_time_visualizer_queue_reload (self);
+          break;
+        }
+    }
+}
diff --git a/src/libsysprof-ui/sysprof-time-visualizer.h b/src/libsysprof-ui/sysprof-time-visualizer.h
new file mode 100644
index 0000000..1b9160a
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-time-visualizer.h
@@ -0,0 +1,54 @@
+/* sysprof-time-visualizer.h
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "sysprof-visualizer.h"
+
+G_BEGIN_DECLS
+
+#define SYSPROF_TYPE_TIME_VISUALIZER (sysprof_time_visualizer_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (SysprofTimeVisualizer, sysprof_time_visualizer, SYSPROF, TIME_VISUALIZER, 
SysprofVisualizer)
+
+struct _SysprofTimeVisualizerClass
+{
+  SysprofVisualizerClass parent_class;
+
+  void (*counter_added) (SysprofTimeVisualizer *self,
+                         guint                counter_id);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+GtkWidget *sysprof_time_visualizer_new            (void);
+void       sysprof_time_visualizer_clear          (SysprofTimeVisualizer *self);
+void       sysprof_time_visualizer_add_counter    (SysprofTimeVisualizer *self,
+                                                   guint                  counter_id,
+                                                   const GdkRGBA         *color);
+void       sysprof_time_visualizer_set_line_width (SysprofTimeVisualizer *self,
+                                                   guint                  counter_id,
+                                                   gdouble                width);
+void       sysprof_time_visualizer_set_dash       (SysprofTimeVisualizer *self,
+                                                   guint                  counter_id,
+                                                   gboolean               use_dash);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-ui-private.h b/src/libsysprof-ui/sysprof-ui-private.h
index 3895683..aef6e8a 100644
--- a/src/libsysprof-ui/sysprof-ui-private.h
+++ b/src/libsysprof-ui/sysprof-ui-private.h
@@ -20,42 +20,18 @@
 
 #pragma once
 
-#include "sysprof-callgraph-view.h"
+#include "sysprof-callgraph-page.h"
 #include "sysprof-display.h"
-#include "sysprof-marks-view.h"
 #include "sysprof-profiler-assistant.h"
-#include "sysprof-visualizer-view.h"
 
 G_BEGIN_DECLS
 
-typedef struct
-{
-  gchar   *name;
-  guint64  count;
-  gint64   max;
-  gint64   min;
-  gint64   avg;
-  guint64  avg_count;
-} SysprofMarkStat;
-
-SysprofMarkStat *_sysprof_mark_stat_new                   (const gchar              *name);
-void             _sysprof_mark_stat_free                  (SysprofMarkStat          *self);
-void             _sysprof_marks_view_set_hadjustment      (SysprofMarksView         *self,
-                                                           GtkAdjustment            *hadjustment);
-void             _sysprof_visualizer_view_set_hadjustment (SysprofVisualizerView    *self,
-                                                           GtkAdjustment            *hadjustment);
-void             _sysprof_rounded_rectangle               (cairo_t                  *cr,
-                                                           const GdkRectangle       *rect,
-                                                           gint                      x_radius,
-                                                           gint                      y_radius);
-gchar           *_sysprof_format_duration                 (gint64                    duration);
-void             _sysprof_callgraph_view_set_failed       (SysprofCallgraphView     *self);
-void             _sysprof_callgraph_view_set_loading      (SysprofCallgraphView     *self,
-                                                           gboolean                  loading);
-void             _sysprof_display_focus_record            (SysprofDisplay           *self);
-void             _sysprof_profiler_assistant_focus_record (SysprofProfilerAssistant *self);
-
-G_DEFINE_AUTOPTR_CLEANUP_FUNC (SysprofMarkStat, _sysprof_mark_stat_free)
+void   _sysprof_callgraph_page_set_failed       (SysprofCallgraphPage     *self);
+void   _sysprof_callgraph_page_set_loading      (SysprofCallgraphPage     *self,
+                                                 gboolean                  loading);
+void   _sysprof_display_focus_record            (SysprofDisplay           *self);
+void   _sysprof_profiler_assistant_focus_record (SysprofProfilerAssistant *self);
+gchar *_sysprof_format_duration                 (gint64                    duration);
 
 #if !GLIB_CHECK_VERSION(2, 56, 0)
 # define g_clear_weak_pointer(ptr) \
diff --git a/src/libsysprof-ui/sysprof-ui.h b/src/libsysprof-ui/sysprof-ui.h
index 4f020d5..c63a712 100644
--- a/src/libsysprof-ui/sysprof-ui.h
+++ b/src/libsysprof-ui/sysprof-ui.h
@@ -27,31 +27,14 @@ G_BEGIN_DECLS
 
 #define SYSPROF_UI_INSIDE
 
-# include "sysprof-aid.h"
-# include "sysprof-callgraph-view.h"
-# include "sysprof-capture-view.h"
-# include "sysprof-callgraph-aid.h"
-# include "sysprof-cell-renderer-percent.h"
 # include "sysprof-check.h"
-# include "sysprof-cpu-aid.h"
-# include "sysprof-cpu-visualizer-row.h"
 # include "sysprof-display.h"
-# include "sysprof-empty-state-view.h"
-# include "sysprof-failed-state-view.h"
-# include "sysprof-line-visualizer-row.h"
-# include "sysprof-marks-model.h"
-# include "sysprof-marks-view.h"
-# include "sysprof-mark-visualizer-row.h"
-# include "sysprof-memory-aid.h"
 # include "sysprof-model-filter.h"
 # include "sysprof-notebook.h"
+# include "sysprof-page.h"
 # include "sysprof-process-model-row.h"
-# include "sysprof-profiler-assistant.h"
-# include "sysprof-proxy-aid.h"
-# include "sysprof-recording-state-view.h"
-# include "sysprof-visualizer-row.h"
-# include "sysprof-visualizer-view.h"
-# include "sysprof-zoom-manager.h"
+# include "sysprof-visualizer-group.h"
+# include "sysprof-visualizer.h"
 
 #undef SYSPROF_UI_INSIDE
 
diff --git a/src/libsysprof-ui/sysprof-visualizer-group-header.c 
b/src/libsysprof-ui/sysprof-visualizer-group-header.c
new file mode 100644
index 0000000..9fc1a43
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-visualizer-group-header.c
@@ -0,0 +1,159 @@
+/* sysprof-visualizer-group-header.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-visualizer-group-header"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "sysprof-visualizer.h"
+#include "sysprof-visualizer-group.h"
+#include "sysprof-visualizer-group-header.h"
+
+struct _SysprofVisualizerGroupHeader
+{
+  GtkListBoxRow parent_instance;
+
+  GtkBox *box;
+};
+
+G_DEFINE_TYPE (SysprofVisualizerGroupHeader, sysprof_visualizer_group_header, GTK_TYPE_LIST_BOX_ROW)
+
+static void
+sysprof_visualizer_group_header_finalize (GObject *object)
+{
+  G_OBJECT_CLASS (sysprof_visualizer_group_header_parent_class)->finalize (object);
+}
+
+static void
+sysprof_visualizer_group_header_class_init (SysprofVisualizerGroupHeaderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = sysprof_visualizer_group_header_finalize;
+}
+
+static void
+sysprof_visualizer_group_header_init (SysprofVisualizerGroupHeader *self)
+{
+  self->box = g_object_new (GTK_TYPE_BOX,
+                            "orientation", GTK_ORIENTATION_VERTICAL,
+                            "visible", TRUE,
+                            NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->box));
+}
+
+void
+_sysprof_visualizer_group_header_add_row (SysprofVisualizerGroupHeader *self,
+                                          guint                         position,
+                                          const gchar                  *title,
+                                          GMenuModel                   *menu,
+                                          GtkWidget                    *widget)
+{
+  GtkBox *box;
+  GtkWidget *group;
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER_GROUP_HEADER (self));
+  g_return_if_fail (SYSPROF_IS_VISUALIZER (widget));
+  g_return_if_fail (!menu || G_IS_MENU_MODEL (menu));
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "orientation", GTK_ORIENTATION_HORIZONTAL,
+                      "spacing", 6,
+                      "visible", TRUE,
+                      NULL);
+  g_object_bind_property (widget, "visible", box, "visible", G_BINDING_SYNC_CREATE);
+  gtk_container_add_with_properties (GTK_CONTAINER (self->box), GTK_WIDGET (box),
+                                     "position", position,
+                                     NULL);
+
+  if (title != NULL)
+    {
+      g_autoptr(GtkSizeGroup) size_group = gtk_size_group_new (GTK_SIZE_GROUP_VERTICAL);
+      PangoAttrList *attrs = pango_attr_list_new ();
+      GtkLabel *label;
+
+      pango_attr_list_insert (attrs, pango_attr_scale_new (0.83333));
+      label = g_object_new (GTK_TYPE_LABEL,
+                            "attributes", attrs,
+                            "ellipsize", PANGO_ELLIPSIZE_MIDDLE,
+                            "margin", 6,
+                            "hexpand", TRUE,
+                            "label", title,
+                            "visible", TRUE,
+                            "xalign", 0.0f,
+                            NULL);
+      gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (label));
+      pango_attr_list_unref (attrs);
+
+      gtk_size_group_add_widget (size_group, widget);
+      gtk_size_group_add_widget (size_group, GTK_WIDGET (box));
+    }
+
+  group = gtk_widget_get_ancestor (widget, SYSPROF_TYPE_VISUALIZER_GROUP);
+
+  if (position == 0 && sysprof_visualizer_group_get_has_page (SYSPROF_VISUALIZER_GROUP (group)))
+    {
+      GtkImage *image;
+
+      image = g_object_new (GTK_TYPE_IMAGE,
+                            "icon-name", "view-paged-symbolic",
+                            "tooltip-text", _("Select for more details"),
+                            "pixel-size", 16,
+                            "visible", TRUE,
+                            NULL);
+      dzl_gtk_widget_add_style_class (GTK_WIDGET (image), "dim-label");
+      gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (image));
+    }
+
+  if (menu != NULL)
+    {
+      GtkStyleContext *style_context;
+      GtkMenuButton *button;
+
+      button = g_object_new (GTK_TYPE_MENU_BUTTON,
+                             "child", g_object_new (GTK_TYPE_IMAGE,
+                                                    "icon-name", "view-more-symbolic",
+                                                    "visible", TRUE,
+                                                    NULL),
+                             "margin-right", 6,
+                             "direction", GTK_ARROW_RIGHT,
+                             "halign", GTK_ALIGN_CENTER,
+                             "menu-model", menu,
+                             "tooltip-text", _("Display supplimental graphs"),
+                             "use-popover", FALSE,
+                             "valign", GTK_ALIGN_CENTER,
+                             "visible", TRUE,
+                             NULL);
+      style_context = gtk_widget_get_style_context (GTK_WIDGET (button));
+      gtk_style_context_add_class (style_context, "image-button");
+      gtk_style_context_add_class (style_context, "small-button");
+      gtk_style_context_add_class (style_context, "flat");
+
+      gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (button));
+    }
+}
+
+SysprofVisualizerGroupHeader *
+_sysprof_visualizer_group_header_new (void)
+{
+  return g_object_new (SYSPROF_TYPE_VISUALIZER_GROUP_HEADER, NULL);
+}
diff --git a/src/libsysprof-ui/sysprof-visualizer-group-header.h 
b/src/libsysprof-ui/sysprof-visualizer-group-header.h
new file mode 100644
index 0000000..2a1e9c8
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-visualizer-group-header.h
@@ -0,0 +1,31 @@
+/* sysprof-visualizer-group-header.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define SYSPROF_TYPE_VISUALIZER_GROUP_HEADER (sysprof_visualizer_group_header_get_type())
+
+G_DECLARE_FINAL_TYPE (SysprofVisualizerGroupHeader, sysprof_visualizer_group_header, SYSPROF, 
VISUALIZER_GROUP_HEADER, GtkListBoxRow)
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-visualizer-group-private.h 
b/src/libsysprof-ui/sysprof-visualizer-group-private.h
new file mode 100644
index 0000000..850c960
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-visualizer-group-private.h
@@ -0,0 +1,48 @@
+/* sysprof-visualizers-group-private.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <sysprof-capture.h>
+
+#include "sysprof-visualizer-group.h"
+#include "sysprof-visualizer-group-header.h"
+
+G_BEGIN_DECLS
+
+void                          _sysprof_visualizer_set_data_width          (SysprofVisualizer            
*self,
+                                                                           gint                          
width);
+void                          _sysprof_visualizer_group_set_data_width    (SysprofVisualizerGroup       
*self,
+                                                                           gint                          
width);
+void                          _sysprof_visualizer_group_set_reader        (SysprofVisualizerGroup       
*self,
+                                                                           SysprofCaptureReader         
*reader);
+SysprofVisualizerGroupHeader *_sysprof_visualizer_group_header_new        (void);
+void                          _sysprof_visualizer_group_header_add_row    (SysprofVisualizerGroupHeader 
*self,
+                                                                           guint                         
position,
+                                                                           const gchar                  
*title,
+                                                                           GMenuModel                   
*menu,
+                                                                           GtkWidget                    
*row);
+void                          _sysprof_visualizer_group_header_remove_row (SysprofVisualizerGroupHeader 
*self,
+                                                                           guint                         
row);
+void                          _sysprof_visualizer_group_set_header        (SysprofVisualizerGroup       
*self,
+                                                                           SysprofVisualizerGroupHeader 
*header);
+
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-visualizer-group.c b/src/libsysprof-ui/sysprof-visualizer-group.c
new file mode 100644
index 0000000..851ffba
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-visualizer-group.c
@@ -0,0 +1,511 @@
+/* sysprof-visualizer-group.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-visualizer-group"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "sysprof-visualizer.h"
+#include "sysprof-visualizer-group.h"
+#include "sysprof-visualizer-group-private.h"
+
+typedef struct
+{
+  /* Owned pointers */
+  GMenuModel                   *menu;
+  GMenu                        *default_menu;
+  GMenu                        *rows_menu;
+  gchar                        *title;
+  GtkSizeGroup                 *size_group;
+  GSimpleActionGroup           *actions;
+
+  gint priority;
+
+  guint has_page : 1;
+
+  /* Weak pointers */
+  SysprofVisualizerGroupHeader *header;
+
+  /* Child Widgets */
+  GtkBox                       *visualizers;
+} SysprofVisualizerGroupPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (SysprofVisualizerGroup, sysprof_visualizer_group, GTK_TYPE_LIST_BOX_ROW)
+
+enum {
+  PROP_0,
+  PROP_HAS_PAGE,
+  PROP_MENU,
+  PROP_PRIORITY,
+  PROP_TITLE,
+  N_PROPS
+};
+
+enum {
+  GROUP_ACTIVATED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+/**
+ * sysprof_visualizer_group_new:
+ *
+ * Create a new #SysprofVisualizerGroup.
+ *
+ * Returns: (transfer full): a newly created #SysprofVisualizerGroup
+ */
+SysprofVisualizerGroup *
+sysprof_visualizer_group_new (void)
+{
+  return g_object_new (SYSPROF_TYPE_VISUALIZER_GROUP, NULL);
+}
+
+const gchar *
+sysprof_visualizer_group_get_title (SysprofVisualizerGroup *self)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZER_GROUP (self), NULL);
+
+  return priv->title;
+}
+
+void
+sysprof_visualizer_group_set_title (SysprofVisualizerGroup *self,
+                                    const gchar            *title)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER_GROUP (self));
+
+  if (g_strcmp0 (priv->title, title) != 0)
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+}
+
+/**
+ * sysprof_visualizer_group_get_menu:
+ *
+ * Gets the menu for the group.
+ *
+ * Returns: (transfer none) (nullable): a #GMenuModel or %NULL
+ *
+ * Since: 3.34
+ */
+GMenuModel *
+sysprof_visualizer_group_get_menu (SysprofVisualizerGroup *self)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZER_GROUP (self), NULL);
+
+  return priv->menu;
+}
+
+void
+sysprof_visualizer_group_set_menu (SysprofVisualizerGroup *self,
+                                   GMenuModel             *menu)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER_GROUP (self));
+  g_return_if_fail (!menu || G_IS_MENU_MODEL (menu));
+
+  if (g_set_object (&priv->menu, menu))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MENU]);
+}
+
+static gchar *
+create_action_name (const gchar *str)
+{
+  GString *ret = g_string_new (NULL);
+
+  for (; *str; str = g_utf8_next_char (str))
+    {
+      gunichar ch = g_utf8_get_char (str);
+
+      if (g_unichar_isalnum (ch))
+        g_string_append_unichar (ret, ch);
+      else
+        g_string_append_c (ret, '_');
+    }
+
+  return g_string_free (ret, FALSE);
+}
+
+static void
+sysprof_visualizer_group_add (GtkContainer *container,
+                              GtkWidget    *child)
+{
+  SysprofVisualizerGroup *self = (SysprofVisualizerGroup *)container;
+
+  g_assert (SYSPROF_IS_VISUALIZER_GROUP (self));
+  g_assert (GTK_IS_WIDGET (child));
+
+  if (SYSPROF_IS_VISUALIZER (child))
+    sysprof_visualizer_group_insert (self, SYSPROF_VISUALIZER (child), -1, FALSE);
+  else
+    GTK_CONTAINER_CLASS (sysprof_visualizer_group_parent_class)->add (container, child);
+}
+
+static void
+sysprof_visualizer_group_finalize (GObject *object)
+{
+  SysprofVisualizerGroup *self = (SysprofVisualizerGroup *)object;
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_clear_pointer (&priv->title, g_free);
+  g_clear_object (&priv->menu);
+  g_clear_object (&priv->size_group);
+  g_clear_object (&priv->default_menu);
+  g_clear_object (&priv->rows_menu);
+  g_clear_object (&priv->actions);
+
+  g_clear_weak_pointer (&priv->header);
+
+  G_OBJECT_CLASS (sysprof_visualizer_group_parent_class)->finalize (object);
+}
+
+static void
+sysprof_visualizer_group_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+  SysprofVisualizerGroup *self = SYSPROF_VISUALIZER_GROUP (object);
+
+  switch (prop_id)
+    {
+    case PROP_HAS_PAGE:
+      g_value_set_boolean (value, sysprof_visualizer_group_get_has_page (self));
+      break;
+
+    case PROP_MENU:
+      g_value_set_object (value, sysprof_visualizer_group_get_menu (self));
+      break;
+
+    case PROP_PRIORITY:
+      g_value_set_int (value, sysprof_visualizer_group_get_priority (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, sysprof_visualizer_group_get_title (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+sysprof_visualizer_group_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+  SysprofVisualizerGroup *self = SYSPROF_VISUALIZER_GROUP (object);
+
+  switch (prop_id)
+    {
+    case PROP_HAS_PAGE:
+      sysprof_visualizer_group_set_has_page (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_MENU:
+      sysprof_visualizer_group_set_menu (self, g_value_get_object (value));
+      break;
+
+    case PROP_PRIORITY:
+      sysprof_visualizer_group_set_priority (self, g_value_get_int (value));
+      break;
+
+    case PROP_TITLE:
+      sysprof_visualizer_group_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+sysprof_visualizer_group_class_init (SysprofVisualizerGroupClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->finalize = sysprof_visualizer_group_finalize;
+  object_class->get_property = sysprof_visualizer_group_get_property;
+  object_class->set_property = sysprof_visualizer_group_set_property;
+
+  container_class->add = sysprof_visualizer_group_add;
+
+  properties [PROP_HAS_PAGE] =
+    g_param_spec_boolean ("has-page",
+                          "Has Page",
+                          "Has Page",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MENU] =
+    g_param_spec_object ("menu",
+                         "Menu",
+                         "Menu",
+                         G_TYPE_MENU_MODEL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PRIORITY] =
+    g_param_spec_int ("priority",
+                      "Priority",
+                      "The Priority of the group, used for sorting",
+                      G_MININT, G_MAXINT, 0,
+                      (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the row",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [GROUP_ACTIVATED] =
+    g_signal_new ("group-activated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE, 0);
+
+  gtk_widget_class_set_css_name (widget_class, "SysprofVisualizerGroup");
+}
+
+static void
+sysprof_visualizer_group_init (SysprofVisualizerGroup *self)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+  g_autoptr(GMenuItem) item = NULL;
+
+  priv->actions = g_simple_action_group_new ();
+
+  priv->default_menu = g_menu_new ();
+  priv->rows_menu = g_menu_new ();
+
+  item = g_menu_item_new_section (NULL, G_MENU_MODEL (priv->rows_menu));
+  g_menu_append_item (priv->default_menu, item);
+
+  priv->menu = g_object_ref (G_MENU_MODEL (priv->default_menu));
+
+  priv->size_group = gtk_size_group_new (GTK_SIZE_GROUP_VERTICAL);
+  gtk_size_group_add_widget (priv->size_group, GTK_WIDGET (self));
+
+  priv->visualizers = g_object_new (GTK_TYPE_BOX,
+                                    "orientation", GTK_ORIENTATION_VERTICAL,
+                                    "visible", TRUE,
+                                    NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (priv->visualizers));
+}
+
+void
+_sysprof_visualizer_group_set_header (SysprofVisualizerGroup       *self,
+                                      SysprofVisualizerGroupHeader *header)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER_GROUP (self));
+  g_return_if_fail (!header || SYSPROF_IS_VISUALIZER_GROUP_HEADER (header));
+
+  if (g_set_weak_pointer (&priv->header, header))
+    {
+      if (header != NULL)
+        {
+          GList *children;
+          guint position = 0;
+
+          gtk_widget_insert_action_group (GTK_WIDGET (header),
+                                          "group",
+                                          G_ACTION_GROUP (priv->actions));
+          gtk_size_group_add_widget (priv->size_group, GTK_WIDGET (header));
+
+          children = gtk_container_get_children (GTK_CONTAINER (priv->visualizers));
+
+          for (const GList *iter = children; iter; iter = iter->next)
+            {
+              SysprofVisualizer *vis = iter->data;
+              const gchar *title;
+              GMenuModel *menu = NULL;
+
+              g_assert (SYSPROF_IS_VISUALIZER (vis));
+
+              if (position == 0)
+                menu = priv->menu;
+
+              title = sysprof_visualizer_get_title (vis);
+
+              if (title == NULL)
+                title = priv->title;
+
+              _sysprof_visualizer_group_header_add_row (header,
+                                                        position,
+                                                        title,
+                                                        menu,
+                                                        GTK_WIDGET (vis));
+
+              position++;
+            }
+
+          g_list_free (children);
+        }
+    }
+}
+
+static void
+sysprof_visualizer_group_set_reader_cb (SysprofVisualizer    *visualizer,
+                                        SysprofCaptureReader *reader)
+{
+  sysprof_visualizer_set_reader (visualizer, reader);
+}
+
+void
+_sysprof_visualizer_group_set_reader (SysprofVisualizerGroup *self,
+                                      SysprofCaptureReader   *reader)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER_GROUP (self));
+  g_return_if_fail (reader != NULL);
+
+  gtk_container_foreach (GTK_CONTAINER (priv->visualizers),
+                         (GtkCallback) sysprof_visualizer_group_set_reader_cb,
+                         reader);
+}
+
+void
+sysprof_visualizer_group_insert (SysprofVisualizerGroup *self,
+                                 SysprofVisualizer      *visualizer,
+                                 gint                    position,
+                                 gboolean                can_toggle)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER_GROUP (self));
+  g_return_if_fail (SYSPROF_IS_VISUALIZER (visualizer));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (priv->visualizers), GTK_WIDGET (visualizer),
+                                     "position", position,
+                                     NULL);
+
+  if (can_toggle)
+    {
+      const gchar *title = sysprof_visualizer_get_title (visualizer);
+      g_autofree gchar *action_name = create_action_name (title);
+      g_autofree gchar *full_action_name = g_strdup_printf ("group.%s", action_name);
+      g_autoptr(GMenuItem) item = g_menu_item_new (title, full_action_name);
+      g_autoptr(GPropertyAction) action = NULL;
+
+      action = g_property_action_new (action_name, visualizer, "visible");
+      g_action_map_add_action (G_ACTION_MAP (priv->actions), G_ACTION (action));
+      g_menu_item_set_attribute (item, "role", "s", "check");
+      g_menu_append_item (priv->rows_menu, item);
+    }
+}
+
+static void
+propagate_data_width_cb (GtkWidget *widget,
+                         gpointer   user_data)
+{
+  _sysprof_visualizer_set_data_width (SYSPROF_VISUALIZER (widget),
+                                      GPOINTER_TO_INT (user_data));
+}
+
+void
+_sysprof_visualizer_group_set_data_width (SysprofVisualizerGroup *self,
+                                          gint                    width)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER_GROUP (self));
+
+  gtk_container_foreach (GTK_CONTAINER (priv->visualizers),
+                         propagate_data_width_cb,
+                         GINT_TO_POINTER (width));
+}
+
+gint
+sysprof_visualizer_group_get_priority (SysprofVisualizerGroup *self)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZER_GROUP (self), 0);
+
+  return priv->priority;
+}
+
+void
+sysprof_visualizer_group_set_priority (SysprofVisualizerGroup *self,
+                                       gint                    priority)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER_GROUP (self));
+
+  if (priv->priority != priority)
+    {
+      priv->priority = priority;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PRIORITY]);
+    }
+}
+
+gboolean
+sysprof_visualizer_group_get_has_page (SysprofVisualizerGroup *self)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZER_GROUP (self), FALSE);
+
+  return priv->has_page;
+}
+
+void
+sysprof_visualizer_group_set_has_page (SysprofVisualizerGroup *self,
+                                       gboolean                has_page)
+{
+  SysprofVisualizerGroupPrivate *priv = sysprof_visualizer_group_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER_GROUP (self));
+
+  has_page = !!has_page;
+
+  if (has_page != priv->has_page)
+    {
+      priv->has_page = has_page;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_PAGE]);
+    }
+}
diff --git a/src/libsysprof-ui/sysprof-visualizer-group.h b/src/libsysprof-ui/sysprof-visualizer-group.h
new file mode 100644
index 0000000..cac1c66
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-visualizer-group.h
@@ -0,0 +1,76 @@
+/* sysprof-visualizer-group.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
+# error "Only <sysprof-ui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+#include "sysprof-visualizer.h"
+
+G_BEGIN_DECLS
+
+#define SYSPROF_TYPE_VISUALIZER_GROUP (sysprof_visualizer_group_get_type())
+
+SYSPROF_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (SysprofVisualizerGroup, sysprof_visualizer_group, SYSPROF, VISUALIZER_GROUP, 
GtkListBoxRow)
+
+struct _SysprofVisualizerGroupClass
+{
+  GtkListBoxRowClass parent_class;
+
+  void (*group_activated) (SysprofVisualizerGroup *self);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+SYSPROF_AVAILABLE_IN_ALL
+SysprofVisualizerGroup *sysprof_visualizer_group_new          (void);
+SYSPROF_AVAILABLE_IN_ALL
+GMenuModel             *sysprof_visualizer_group_get_menu     (SysprofVisualizerGroup *self);
+SYSPROF_AVAILABLE_IN_ALL
+void                    sysprof_visualizer_group_set_menu     (SysprofVisualizerGroup *self,
+                                                               GMenuModel             *menu);
+SYSPROF_AVAILABLE_IN_ALL
+gint                    sysprof_visualizer_group_get_priority (SysprofVisualizerGroup *self);
+SYSPROF_AVAILABLE_IN_ALL
+void                    sysprof_visualizer_group_set_priority (SysprofVisualizerGroup *self,
+                                                               gint                    priority);
+SYSPROF_AVAILABLE_IN_ALL
+const gchar            *sysprof_visualizer_group_get_title    (SysprofVisualizerGroup *self);
+SYSPROF_AVAILABLE_IN_ALL
+void                    sysprof_visualizer_group_set_title    (SysprofVisualizerGroup *self,
+                                                               const gchar            *title);
+SYSPROF_AVAILABLE_IN_ALL
+gboolean                sysprof_visualizer_group_get_has_page (SysprofVisualizerGroup *self);
+SYSPROF_AVAILABLE_IN_ALL
+void                    sysprof_visualizer_group_set_has_page (SysprofVisualizerGroup *self,
+                                                               gboolean                has_page);
+SYSPROF_AVAILABLE_IN_ALL
+void                    sysprof_visualizer_group_insert       (SysprofVisualizerGroup *self,
+                                                               SysprofVisualizer      *visualizer,
+                                                               gint                    position,
+                                                               gboolean                can_toggle);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-visualizer-ticks.c b/src/libsysprof-ui/sysprof-visualizer-ticks.c
index 4cbf291..fc3a60a 100644
--- a/src/libsysprof-ui/sysprof-visualizer-ticks.c
+++ b/src/libsysprof-ui/sysprof-visualizer-ticks.c
@@ -18,19 +18,24 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
+#define G_LOG_DOMAIN "sysprof-visualizer-ticks"
+
 #include "config.h"
 
 #include <glib/gi18n.h>
+#include <sysprof.h>
 
 #include "sysprof-visualizer-ticks.h"
 
 #define NSEC_PER_SEC G_GINT64_CONSTANT(1000000000)
-#define NSEC_PER_HOUR (NSEC_PER_SEC * 60 * 60)
-#define NSEC_PER_MIN (NSEC_PER_SEC * 60)
+#define NSEC_PER_DAY (NSEC_PER_SEC * 60L * 60L * 24L)
+#define NSEC_PER_HOUR (NSEC_PER_SEC * 60L * 60L)
+#define NSEC_PER_MIN (NSEC_PER_SEC * 60L)
 #define NSEC_PER_MSEC (NSEC_PER_SEC/G_GINT64_CONSTANT(1000))
 #define MIN_TICK_DISTANCE 20
-#define LABEL_HEIGHT_PX 8
+#define LABEL_HEIGHT_PX 10
 
+SYSPROF_ALIGNED_BEGIN (8)
 struct _SysprofVisualizerTicks
 {
   GtkDrawingArea parent_instance;
@@ -38,7 +43,7 @@ struct _SysprofVisualizerTicks
   gint64 epoch;
   gint64 begin_time;
   gint64 end_time;
-} __attribute__((aligned(8)));
+} SYSPROF_ALIGNED_END (8);
 
 enum {
   TICK_MINUTES,
@@ -50,6 +55,7 @@ enum {
   TICK_TENTHS,
   TICK_HUNDREDTHS,
   TICK_THOUSANDTHS,
+  TICK_TEN_THOUSANDTHS,
   N_TICKS
 };
 
@@ -67,6 +73,7 @@ struct {
   { 1, 5, NSEC_PER_SEC / 10 },
   { 1, 4, NSEC_PER_SEC / 100 },
   { 1, 3, NSEC_PER_SEC / 1000 },
+  { 1, 1, NSEC_PER_SEC / 10000 },
 };
 
 G_DEFINE_TYPE (SysprofVisualizerTicks, sysprof_visualizer_ticks, GTK_TYPE_DRAWING_AREA)
@@ -82,6 +89,7 @@ update_label_text (PangoLayout *layout,
   gint hours = 0;
   gint min = 0;
   gint sec = 0;
+  G_GNUC_UNUSED gint days = 0;
 
   g_assert (PANGO_IS_LAYOUT (layout));
 
@@ -89,6 +97,12 @@ update_label_text (PangoLayout *layout,
   time -= tmp;
   msec = tmp / 100000L;
 
+  if (time >= NSEC_PER_DAY)
+    {
+      days = time / NSEC_PER_DAY;
+      time %= NSEC_PER_DAY;
+    }
+
   if (time >= NSEC_PER_HOUR)
     {
       hours = time / NSEC_PER_HOUR;
@@ -135,18 +149,6 @@ get_x_for_time (SysprofVisualizerTicks   *self,
   return alloc->width * x_ratio;
 }
 
-#if 0
-static inline gint64
-get_time_at_x (SysprofVisualizerTicks   *self,
-               const GtkAllocation *alloc,
-               gdouble              x)
-{
-  return self->begin_time
-       - self->epoch
-       + ((self->end_time - self->begin_time) / (gdouble)alloc->width * x);
-}
-#endif
-
 static gboolean
 draw_ticks (SysprofVisualizerTicks *self,
             cairo_t           *cr,
@@ -186,8 +188,10 @@ draw_ticks (SysprofVisualizerTicks *self,
       PangoLayout *layout;
       PangoFontDescription *font_desc;
       gboolean want_msec;
+      gint last_x2 = G_MININT;
+      gint w, h;
 
-      layout = gtk_widget_create_pango_layout (GTK_WIDGET (self), "00:10:00");
+      layout = gtk_widget_create_pango_layout (GTK_WIDGET (self), "00:10:00.0000");
 
       font_desc = pango_font_description_new ();
       pango_font_description_set_family_static (font_desc, "Monospace");
@@ -195,6 +199,8 @@ draw_ticks (SysprofVisualizerTicks *self,
       pango_layout_set_font_description (layout, font_desc);
       pango_font_description_free (font_desc);
 
+      pango_layout_get_pixel_size (layout, &w, &h);
+
       /* If we are operating on smaller than seconds here, then we want
        * to ensure we include msec with the timestamps.
        */
@@ -206,9 +212,15 @@ draw_ticks (SysprofVisualizerTicks *self,
         {
           gdouble x = get_x_for_time (self, &alloc, t);
 
-          cairo_move_to (cr, (gint)x + .5 - (gint)half, alloc.height - LABEL_HEIGHT_PX);
+          if (x < (last_x2 + MIN_TICK_DISTANCE))
+            continue;
+
+          cairo_move_to (cr, (gint)x + 2.5 - (gint)half, 2);
           update_label_text (layout, t - self->epoch, want_msec);
+          pango_layout_get_pixel_size (layout, &w, &h);
           pango_cairo_show_layout (cr, layout);
+
+          last_x2 = x + w;
         }
 
       g_clear_object (&layout);
@@ -221,8 +233,8 @@ draw_ticks (SysprofVisualizerTicks *self,
         {
           gdouble x = get_x_for_time (self, &alloc, t);
 
-          cairo_move_to (cr, (gint)x - .5 - (gint)half, 0);
-          cairo_line_to (cr, (gint)x - .5 - (gint)half, tick_sizing[ticks].height);
+          cairo_move_to (cr, (gint)x - .5 - (gint)half, alloc.height);
+          cairo_line_to (cr, (gint)x - .5 - (gint)half, alloc.height - tick_sizing[ticks].height);
           count++;
         }
 
@@ -235,7 +247,7 @@ draw_ticks (SysprofVisualizerTicks *self,
 
 static gboolean
 sysprof_visualizer_ticks_draw (GtkWidget *widget,
-                          cairo_t   *cr)
+                               cairo_t   *cr)
 {
   SysprofVisualizerTicks *self = SYSPROF_VISUALIZER_TICKS (widget);
   GtkStyleContext *style;
@@ -250,9 +262,12 @@ sysprof_visualizer_ticks_draw (GtkWidget *widget,
   if (0 == (timespan = self->end_time - self->begin_time))
     return GDK_EVENT_PROPAGATE;
 
+  style = gtk_widget_get_style_context (widget);
+
   gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
 
-  style = gtk_widget_get_style_context (widget);
+  gtk_render_background (style, cr, 0, 0, alloc.width, alloc.height);
+
   state = gtk_widget_get_state_flags (widget);
   gtk_style_context_get_color (style, state, &color);
 
@@ -290,8 +305,8 @@ sysprof_visualizer_ticks_draw (GtkWidget *widget,
 
 static void
 sysprof_visualizer_ticks_get_preferred_height (GtkWidget *widget,
-                                          gint      *min_height,
-                                          gint      *nat_height)
+                                               gint      *min_height,
+                                               gint      *nat_height)
 {
   g_assert (SYSPROF_IS_VISUALIZER_TICKS (widget));
 
@@ -306,7 +321,7 @@ sysprof_visualizer_ticks_class_init (SysprofVisualizerTicksClass *klass)
   widget_class->draw = sysprof_visualizer_ticks_draw;
   widget_class->get_preferred_height = sysprof_visualizer_ticks_get_preferred_height;
 
-  gtk_widget_class_set_css_name (widget_class, "ticks");
+  gtk_widget_class_set_css_name (widget_class, "SysprofVisualizerTicks");
 }
 
 static void
@@ -325,8 +340,8 @@ sysprof_visualizer_ticks_new (void)
 
 void
 sysprof_visualizer_ticks_get_time_range (SysprofVisualizerTicks *self,
-                                    gint64            *begin_time,
-                                    gint64            *end_time)
+                                         gint64                 *begin_time,
+                                         gint64                 *end_time)
 {
   g_return_if_fail (SYSPROF_IS_VISUALIZER_TICKS (self));
   g_return_if_fail (begin_time != NULL || end_time != NULL);
@@ -340,8 +355,8 @@ sysprof_visualizer_ticks_get_time_range (SysprofVisualizerTicks *self,
 
 void
 sysprof_visualizer_ticks_set_time_range (SysprofVisualizerTicks *self,
-                                    gint64             begin_time,
-                                    gint64             end_time)
+                                         gint64                  begin_time,
+                                         gint64                  end_time)
 {
   g_return_if_fail (SYSPROF_IS_VISUALIZER_TICKS (self));
 
@@ -390,3 +405,11 @@ sysprof_visualizer_ticks_set_epoch (SysprofVisualizerTicks *self,
       gtk_widget_queue_draw (GTK_WIDGET (self));
     }
 }
+
+gint64
+sysprof_visualizer_ticks_get_duration (SysprofVisualizerTicks *self)
+{
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZER_TICKS (self), 0);
+
+  return self->end_time - self->begin_time;
+}
diff --git a/src/libsysprof-ui/sysprof-visualizer-ticks.h b/src/libsysprof-ui/sysprof-visualizer-ticks.h
index 2ef71e7..aa963fa 100644
--- a/src/libsysprof-ui/sysprof-visualizer-ticks.h
+++ b/src/libsysprof-ui/sysprof-visualizer-ticks.h
@@ -20,10 +20,6 @@
 
 #pragma once
 
-#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
-# error "Only <sysprof-ui.h> can be included directly."
-#endif
-
 #include <gtk/gtk.h>
 
 G_BEGIN_DECLS
@@ -42,5 +38,6 @@ void       sysprof_visualizer_ticks_get_time_range (SysprofVisualizerTicks *self
 void       sysprof_visualizer_ticks_set_time_range (SysprofVisualizerTicks *self,
                                                     gint64                  begin_time,
                                                     gint64                  end_time);
+gint64     sysprof_visualizer_ticks_get_duration   (SysprofVisualizerTicks *self);
 
 G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-visualizer.c b/src/libsysprof-ui/sysprof-visualizer.c
new file mode 100644
index 0000000..41ffba5
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-visualizer.c
@@ -0,0 +1,346 @@
+/* sysprof-visualizer.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-visualizer"
+
+#include "config.h"
+
+#include "sysprof-visualizer.h"
+
+typedef struct
+{
+  gchar *title;
+
+  gint64 begin_time;
+  gint64 end_time;
+  gint64 duration;
+
+  /* The width for [begin_time..end_time] which may be less
+   * than what the widgets allocation is.
+   */
+  gint data_width;
+} SysprofVisualizerPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (SysprofVisualizer, sysprof_visualizer, DZL_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_BEGIN_TIME,
+  PROP_END_TIME,
+  PROP_TITLE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static gboolean
+sysprof_visualizer_draw (GtkWidget *widget,
+                         cairo_t   *cr)
+{
+  g_assert (SYSPROF_IS_VISUALIZER (widget));
+  g_assert (cr != NULL);
+
+  GTK_WIDGET_CLASS (sysprof_visualizer_parent_class)->draw (widget, cr);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+sysprof_visualizer_get_preferred_width (GtkWidget *widget,
+                                        gint      *min_width,
+                                        gint      *nat_width)
+{
+  SysprofVisualizer *self = (SysprofVisualizer *)widget;
+  SysprofVisualizerPrivate *priv = sysprof_visualizer_get_instance_private (self);
+
+  g_assert (SYSPROF_IS_VISUALIZER (self));
+  g_assert (min_width != NULL);
+  g_assert (nat_width != NULL);
+
+  *min_width = *nat_width = priv->data_width ? priv->data_width : 1;
+}
+
+static void
+sysprof_visualizer_finalize (GObject *object)
+{
+  SysprofVisualizer *self = (SysprofVisualizer *)object;
+  SysprofVisualizerPrivate *priv = sysprof_visualizer_get_instance_private (self);
+
+  g_clear_pointer (&priv->title, g_free);
+
+  G_OBJECT_CLASS (sysprof_visualizer_parent_class)->finalize (object);
+}
+
+static void
+sysprof_visualizer_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  SysprofVisualizer *self = SYSPROF_VISUALIZER (object);
+
+  switch (prop_id)
+    {
+    case PROP_TITLE:
+      g_value_set_string (value, sysprof_visualizer_get_title (self));
+      break;
+
+    case PROP_BEGIN_TIME:
+      g_value_set_int64 (value, sysprof_visualizer_get_begin_time (self));
+      break;
+
+    case PROP_END_TIME:
+      g_value_set_int64 (value, sysprof_visualizer_get_end_time (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+sysprof_visualizer_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  SysprofVisualizer *self = SYSPROF_VISUALIZER (object);
+  SysprofVisualizerPrivate *priv = sysprof_visualizer_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_TITLE:
+      sysprof_visualizer_set_title (self, g_value_get_string (value));
+      break;
+
+    case PROP_BEGIN_TIME:
+      priv->begin_time = g_value_get_int64 (value);
+      priv->duration = priv->end_time - priv->begin_time;
+      break;
+
+    case PROP_END_TIME:
+      priv->end_time = g_value_get_int64 (value);
+      priv->duration = priv->end_time - priv->begin_time;
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+sysprof_visualizer_class_init (SysprofVisualizerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = sysprof_visualizer_finalize;
+  object_class->get_property = sysprof_visualizer_get_property;
+  object_class->set_property = sysprof_visualizer_set_property;
+
+  widget_class->draw = sysprof_visualizer_draw;
+  widget_class->get_preferred_width = sysprof_visualizer_get_preferred_width;
+
+  properties [PROP_BEGIN_TIME] =
+    g_param_spec_int64 ("begin-time",
+                        "Begin Time",
+                        "Begin Time",
+                        G_MININT64,
+                        G_MAXINT64,
+                        0,
+                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_END_TIME] =
+    g_param_spec_int64 ("end-time",
+                        "End Time",
+                        "End Time",
+                        G_MININT64,
+                        G_MAXINT64,
+                        0,
+                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title for the row",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "SysprofVisualizer");
+}
+
+static void
+sysprof_visualizer_init (SysprofVisualizer *self)
+{
+}
+
+const gchar *
+sysprof_visualizer_get_title (SysprofVisualizer *self)
+{
+  SysprofVisualizerPrivate *priv = sysprof_visualizer_get_instance_private (self);
+
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZER (self), 0);
+
+  return priv->title;
+}
+
+void
+sysprof_visualizer_set_title (SysprofVisualizer *self,
+                              const gchar       *title)
+{
+  SysprofVisualizerPrivate *priv = sysprof_visualizer_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER (self));
+
+  if (g_strcmp0 (priv->title, title) != 0)
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+}
+
+gint64
+sysprof_visualizer_get_begin_time (SysprofVisualizer *self)
+{
+  SysprofVisualizerPrivate *priv = sysprof_visualizer_get_instance_private (self);
+
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZER (self), 0);
+
+  return priv->begin_time;
+}
+
+gint64
+sysprof_visualizer_get_end_time (SysprofVisualizer *self)
+{
+  SysprofVisualizerPrivate *priv = sysprof_visualizer_get_instance_private (self);
+
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZER (self), 0);
+
+  return priv->end_time;
+}
+
+void
+sysprof_visualizer_set_reader (SysprofVisualizer    *self,
+                               SysprofCaptureReader *reader)
+{
+  SysprofVisualizerPrivate *priv = sysprof_visualizer_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER (self));
+  g_return_if_fail (reader != NULL);
+
+  if (priv->begin_time == 0 || priv->end_time == 0)
+    {
+      priv->begin_time = sysprof_capture_reader_get_start_time (reader);
+      priv->end_time = sysprof_capture_reader_get_end_time (reader);
+      priv->duration = priv->end_time - priv->begin_time;
+    }
+
+  if (SYSPROF_VISUALIZER_GET_CLASS (self)->set_reader)
+    SYSPROF_VISUALIZER_GET_CLASS (self)->set_reader (self, reader);
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static inline void
+subtract_border (GtkAllocation *alloc,
+                 GtkBorder     *border)
+{
+#if 0
+  g_print ("Border; %d %d %d %d\n", border->top, border->left, border->bottom, border->right);
+#endif
+
+  alloc->x += border->left;
+  alloc->y += border->top;
+  alloc->width -= border->left + border->right;
+  alloc->height -= border->top + border->bottom;
+}
+
+static void
+adjust_alloc_for_borders (SysprofVisualizer *self,
+                          GtkAllocation     *alloc)
+{
+  GtkStyleContext *style_context;
+  GtkBorder border;
+  GtkStateFlags state;
+
+  g_assert (SYSPROF_IS_VISUALIZER (self));
+  g_assert (alloc != NULL);
+
+  state = gtk_widget_get_state_flags (GTK_WIDGET (self));
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  gtk_style_context_get_border (style_context, state, &border);
+
+  subtract_border (alloc, &border);
+}
+
+void
+sysprof_visualizer_translate_points (SysprofVisualizer                    *self,
+                                     const SysprofVisualizerRelativePoint *in_points,
+                                     guint                                 n_in_points,
+                                     SysprofVisualizerAbsolutePoint       *out_points,
+                                     guint                                 n_out_points)
+{
+  SysprofVisualizerPrivate *priv = sysprof_visualizer_get_instance_private (self);
+  GtkAllocation alloc;
+  gint graph_width;
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER (self));
+  g_return_if_fail (in_points != NULL);
+  g_return_if_fail (out_points != NULL);
+  g_return_if_fail (n_in_points == n_out_points);
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+  adjust_alloc_for_borders (self, &alloc);
+
+  graph_width = priv->data_width;
+
+  for (guint i = 0; i < n_in_points; i++)
+    {
+      out_points[i].x = (in_points[i].x * graph_width);
+      out_points[i].y = alloc.height - (ABS (in_points[i].y) * alloc.height);
+    }
+}
+
+void
+_sysprof_visualizer_set_data_width (SysprofVisualizer *self,
+                                    gint               data_width)
+{
+  SysprofVisualizerPrivate *priv = sysprof_visualizer_get_instance_private (self);
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZER (self));
+
+  if (priv->data_width != data_width)
+    {
+      priv->data_width = data_width;
+      gtk_widget_queue_resize (GTK_WIDGET (self));
+    }
+}
+
+gint
+sysprof_visualizer_get_x_for_time (SysprofVisualizer *self,
+                                   gint64             time)
+{
+  SysprofVisualizerPrivate *priv = sysprof_visualizer_get_instance_private (self);
+
+  return ((time - priv->begin_time) / (gdouble)priv->duration) * priv->data_width;
+}
diff --git a/src/libsysprof-ui/sysprof-visualizer.h b/src/libsysprof-ui/sysprof-visualizer.h
new file mode 100644
index 0000000..618f82d
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-visualizer.h
@@ -0,0 +1,82 @@
+/* sysprof-visualizer.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
+# error "Only <sysprof-ui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <sysprof.h>
+
+G_BEGIN_DECLS
+
+typedef struct
+{
+  gdouble x;
+  gdouble y;
+} SysprofVisualizerRelativePoint;
+
+typedef struct
+{
+  gint x;
+  gint y;
+} SysprofVisualizerAbsolutePoint;
+
+#define SYSPROF_TYPE_VISUALIZER (sysprof_visualizer_get_type())
+
+SYSPROF_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (SysprofVisualizer, sysprof_visualizer, SYSPROF, VISUALIZER, DzlBin)
+
+struct _SysprofVisualizerClass
+{
+  DzlBinClass parent_class;
+
+  void (*set_reader) (SysprofVisualizer    *self,
+                      SysprofCaptureReader *reader);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+SYSPROF_AVAILABLE_IN_ALL
+const gchar *sysprof_visualizer_get_title        (SysprofVisualizer                    *self);
+SYSPROF_AVAILABLE_IN_ALL
+void         sysprof_visualizer_set_title        (SysprofVisualizer                    *self,
+                                                  const gchar                          *title);
+SYSPROF_AVAILABLE_IN_ALL
+gint64       sysprof_visualizer_get_begin_time   (SysprofVisualizer                    *self);
+SYSPROF_AVAILABLE_IN_ALL
+gint64       sysprof_visualizer_get_end_time     (SysprofVisualizer                    *self);
+SYSPROF_AVAILABLE_IN_ALL
+void         sysprof_visualizer_set_reader       (SysprofVisualizer                    *self,
+                                                  SysprofCaptureReader                 *reader);
+SYSPROF_AVAILABLE_IN_ALL
+gint         sysprof_visualizer_get_x_for_time   (SysprofVisualizer                    *self,
+                                                  gint64                                time);
+SYSPROF_AVAILABLE_IN_ALL
+void         sysprof_visualizer_translate_points (SysprofVisualizer                    *self,
+                                                  const SysprofVisualizerRelativePoint *in_points,
+                                                  guint                                 n_in_points,
+                                                  SysprofVisualizerAbsolutePoint       *out_points,
+                                                  guint                                 n_out_points);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-visualizers-frame.c b/src/libsysprof-ui/sysprof-visualizers-frame.c
new file mode 100644
index 0000000..a116920
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-visualizers-frame.c
@@ -0,0 +1,767 @@
+/* sysprof-visualizers-frame.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sysprof-visualizers-frame"
+
+#include "config.h"
+
+#include "sysprof-scrollmap.h"
+#include "sysprof-visualizer-group-private.h"
+#include "sysprof-visualizer-ticks.h"
+#include "sysprof-visualizers-frame.h"
+#include "sysprof-zoom-manager.h"
+
+struct _SysprofVisualizersFrame
+{
+  GtkBin                  parent_instance;
+
+  /* Drag selection tracking */
+  SysprofSelection       *selection;
+  gint64                  drag_begin_at;
+  gint64                  drag_selection_at;
+  guint                   button_pressed : 1;
+
+  /* Help avoid over-resizing/allocating */
+  GtkAllocation           last_alloc;
+  gdouble                 last_zoom;
+
+  /* Known time range from the capture */
+  gint64                  begin_time;
+  gint64                  end_time;
+
+  /* Template Widgets */
+  GtkListBox             *groups;
+  GtkListBox             *visualizers;
+  SysprofScrollmap       *hscrollbar;
+  SysprofVisualizerTicks *ticks;
+  GtkScrolledWindow      *ticks_scroller;
+  GtkScrolledWindow      *hscroller;
+  GtkScrolledWindow      *vscroller;
+  SysprofZoomManager     *zoom_manager;
+  GtkScale               *zoom_scale;
+  GtkSizeGroup           *left_column;
+};
+
+typedef struct
+{
+  GtkListBox      *list;
+  GtkStyleContext *style_context;
+  cairo_t         *cr;
+  GtkAllocation    alloc;
+  gint64           begin_time;
+  gint64           duration;
+} SelectionDraw;
+
+G_DEFINE_TYPE (SysprofVisualizersFrame, sysprof_visualizers_frame, GTK_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_SELECTED_GROUP,
+  PROP_SELECTION,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static gint64
+get_time_from_x (SysprofVisualizersFrame *self,
+                 gdouble                  x)
+{
+  GtkAllocation alloc;
+  gdouble ratio;
+  gint64 duration;
+
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (self));
+
+  gtk_widget_get_allocation (GTK_WIDGET (self->ticks), &alloc);
+  duration = sysprof_visualizer_ticks_get_duration (self->ticks);
+
+  if (alloc.width < 1)
+    return 0;
+
+  ratio = x / alloc.width;
+
+  return self->begin_time + (ratio * duration);
+}
+
+static void
+draw_selection_cb (SysprofSelection *selection,
+                   gint64            range_begin,
+                   gint64            range_end,
+                   gpointer          user_data)
+{
+  SelectionDraw *draw = user_data;
+  GdkRectangle area;
+  gdouble x, x2;
+
+  g_assert (SYSPROF_IS_SELECTION (selection));
+  g_assert (draw != NULL);
+  g_assert (draw->cr != NULL);
+  g_assert (GTK_IS_LIST_BOX (draw->list));
+
+  x = (range_begin - draw->begin_time) / (gdouble)draw->duration;
+  x2 = (range_end - draw->begin_time) / (gdouble)draw->duration;
+
+  area.x = x * draw->alloc.width;
+  area.width = (x2 * draw->alloc.width) - area.x;
+  area.y = 0;
+  area.height = draw->alloc.height;
+
+  if (area.width < 0)
+    {
+      area.width = ABS (area.width);
+      area.x -= area.width;
+    }
+
+  gtk_render_background (draw->style_context, draw->cr, area.x + 2, area.y + 2, area.width - 4, area.height 
- 4);
+}
+
+static gboolean
+visualizers_draw_after_cb (SysprofVisualizersFrame *self,
+                           cairo_t                 *cr,
+                           GtkListBox              *list)
+{
+  SelectionDraw draw;
+
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (self));
+  g_assert (GTK_IS_LIST_BOX (list));
+
+  draw.style_context = gtk_widget_get_style_context (GTK_WIDGET (list));
+  draw.list = list;
+  draw.cr = cr;
+  draw.begin_time = self->begin_time;
+  draw.duration = sysprof_visualizer_ticks_get_duration (self->ticks);
+
+  if (draw.duration == 0)
+    return GDK_EVENT_PROPAGATE;
+
+  gtk_widget_get_allocation (GTK_WIDGET (list), &draw.alloc);
+
+  if (sysprof_selection_get_has_selection (self->selection) || self->button_pressed)
+    {
+      gtk_style_context_add_class (draw.style_context, "selection");
+      sysprof_selection_foreach (self->selection, draw_selection_cb, &draw);
+      if (self->button_pressed)
+        draw_selection_cb (self->selection, self->drag_begin_at, self->drag_selection_at, &draw);
+      gtk_style_context_remove_class (draw.style_context, "selection");
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+visualizers_realize_after_cb (SysprofVisualizersFrame *self,
+                              GtkListBox              *list)
+{
+  GdkDisplay *display;
+  GdkWindow *window;
+  GdkCursor *cursor;
+
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (self));
+  g_assert (GTK_IS_LIST_BOX (list));
+
+  window = gtk_widget_get_window (GTK_WIDGET (list));
+  display = gdk_window_get_display (window);
+  cursor = gdk_cursor_new_from_name (display, "text");
+  gdk_window_set_cursor (window, cursor);
+  g_clear_object (&cursor);
+}
+
+static gboolean
+visualizers_button_press_event_cb (SysprofVisualizersFrame *self,
+                                   GdkEventButton          *ev,
+                                   GtkListBox              *visualizers)
+{
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (self));
+  g_assert (ev != NULL);
+  g_assert (GTK_IS_LIST_BOX (visualizers));
+
+  if (ev->button != GDK_BUTTON_PRIMARY)
+    {
+      if (sysprof_selection_get_has_selection (self->selection))
+        {
+          sysprof_selection_unselect_all (self->selection);
+          return GDK_EVENT_STOP;
+        }
+
+      return GDK_EVENT_PROPAGATE;
+    }
+
+  if ((ev->state & GDK_SHIFT_MASK) == 0)
+    sysprof_selection_unselect_all (self->selection);
+
+  self->button_pressed = TRUE;
+
+  self->drag_begin_at = get_time_from_x (self, ev->x);
+  self->drag_selection_at = self->drag_begin_at;
+
+  gtk_widget_queue_draw (GTK_WIDGET (visualizers));
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+visualizers_button_release_event_cb (SysprofVisualizersFrame *self,
+                                     GdkEventButton          *ev,
+                                     GtkListBox              *list)
+{
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (self));
+  g_assert (ev != NULL);
+  g_assert (GTK_IS_LIST_BOX (list));
+
+  if (!self->button_pressed || ev->button != GDK_BUTTON_PRIMARY)
+    return GDK_EVENT_PROPAGATE;
+
+  self->button_pressed = FALSE;
+
+  if (self->drag_begin_at != self->drag_selection_at)
+    {
+      sysprof_selection_select_range (self->selection,
+                                      self->drag_begin_at,
+                                      self->drag_selection_at);
+      self->drag_begin_at = -1;
+      self->drag_selection_at = -1;
+    }
+
+  gtk_widget_queue_draw (GTK_WIDGET (list));
+
+  return GDK_EVENT_STOP;
+}
+
+static gboolean
+visualizers_motion_notify_event_cb (SysprofVisualizersFrame *self,
+                                    GdkEventMotion          *ev,
+                                    GtkListBox              *list)
+{
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (self));
+  g_assert (ev != NULL);
+  g_assert (GTK_IS_LIST_BOX (list));
+
+  if (!self->button_pressed)
+    return GDK_EVENT_PROPAGATE;
+
+  self->drag_selection_at = get_time_from_x (self, ev->x);
+
+  gtk_widget_queue_draw (GTK_WIDGET (list));
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+propagate_data_width_cb (GtkWidget *widget,
+                         gpointer   user_data)
+{
+  _sysprof_visualizer_group_set_data_width (SYSPROF_VISUALIZER_GROUP (widget),
+                                            GPOINTER_TO_INT (user_data));
+}
+
+static void
+sysprof_visualizers_frame_notify_zoom (SysprofVisualizersFrame *self,
+                                       GParamSpec              *pspec,
+                                       SysprofZoomManager      *zoom_manager)
+{
+  gint data_width;
+
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (self));
+  g_assert (SYSPROF_IS_ZOOM_MANAGER (zoom_manager));
+
+  data_width = sysprof_zoom_manager_get_width_for_duration (self->zoom_manager,
+                                                            self->end_time - self->begin_time);
+  gtk_container_foreach (GTK_CONTAINER (self->visualizers),
+                         propagate_data_width_cb,
+                         GINT_TO_POINTER (data_width));
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static void
+sysprof_visualizers_frame_apply_zoom (SysprofVisualizersFrame *self,
+                                      const GtkAllocation     *alloc)
+{
+  gint64 duration;
+  gint64 end_time;
+
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (self));
+
+  duration = sysprof_zoom_manager_get_duration_for_width (self->zoom_manager, alloc->width);
+  end_time = self->begin_time + duration;
+
+  sysprof_scrollmap_set_time_range (self->hscrollbar,
+                                    self->begin_time,
+                                    MAX (self->end_time, end_time));
+  sysprof_visualizer_ticks_set_epoch (self->ticks, self->begin_time);
+  sysprof_visualizer_ticks_set_time_range (self->ticks, self->begin_time, end_time);
+}
+
+static gint
+find_pos (SysprofVisualizersFrame *self,
+          const gchar             *title,
+          gint                     priority)
+{
+  GList *list;
+  gint pos = 0;
+
+  if (title == NULL)
+    return -1;
+
+  list = gtk_container_get_children (GTK_CONTAINER (self->visualizers));
+
+  for (const GList *iter = list; iter; iter = iter->next)
+    {
+      SysprofVisualizerGroup *group = iter->data;
+      gint prio = sysprof_visualizer_group_get_priority (group);
+      const gchar *item = sysprof_visualizer_group_get_title (group);
+
+      if (priority < prio ||
+          (priority == prio && g_strcmp0 (title, item) < 0))
+        break;
+
+      pos++;
+    }
+
+  g_list_free (list);
+
+  return pos;
+}
+
+static void
+sysprof_visualizers_frame_add (GtkContainer *container,
+                               GtkWidget    *child)
+{
+  SysprofVisualizersFrame *self = (SysprofVisualizersFrame *)container;
+
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (self));
+  g_assert (GTK_IS_WIDGET (child));
+
+  if (SYSPROF_IS_VISUALIZER_GROUP (child))
+    {
+      SysprofVisualizerGroupHeader *header;
+      const gchar *title = sysprof_visualizer_group_get_title (SYSPROF_VISUALIZER_GROUP (child));
+      gint priority = sysprof_visualizer_group_get_priority (SYSPROF_VISUALIZER_GROUP (child));
+      gint pos = find_pos (self, title, priority);
+
+      gtk_list_box_insert (self->visualizers, child, pos);
+
+      header = _sysprof_visualizer_group_header_new ();
+      g_object_set_data (G_OBJECT (header), "VISUALIZER_GROUP", child);
+      gtk_list_box_insert (self->groups, GTK_WIDGET (header), pos);
+      _sysprof_visualizer_group_set_header (SYSPROF_VISUALIZER_GROUP (child), header);
+      gtk_widget_show (GTK_WIDGET (header));
+
+      sysprof_visualizers_frame_notify_zoom (self, NULL, self->zoom_manager);
+
+      return;
+    }
+
+  GTK_CONTAINER_CLASS (sysprof_visualizers_frame_parent_class)->add (container, child);
+}
+
+static void
+sysprof_visualizers_frame_size_allocate (GtkWidget     *widget,
+                                         GtkAllocation *alloc)
+{
+  SysprofVisualizersFrame *self = (SysprofVisualizersFrame *)widget;
+  gdouble zoom;
+
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (self));
+  g_assert (alloc != NULL);
+
+  GTK_WIDGET_CLASS (sysprof_visualizers_frame_parent_class)->size_allocate (widget, alloc);
+
+  zoom = sysprof_zoom_manager_get_zoom (self->zoom_manager);
+
+  if (alloc->width != self->last_alloc.width || zoom != self->last_zoom)
+    sysprof_visualizers_frame_apply_zoom (self, alloc);
+
+  self->last_alloc = *alloc;
+  self->last_zoom = zoom;
+}
+
+static void
+sysprof_visualizers_frame_selection_changed (SysprofVisualizersFrame *self,
+                                             SysprofSelection        *selection)
+{
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (self));
+  g_assert (SYSPROF_IS_SELECTION (selection));
+
+  gtk_widget_queue_draw (GTK_WIDGET (self->visualizers));
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SELECTION]);
+}
+
+static void
+sysprof_visualizers_frame_group_activated_cb (SysprofVisualizersFrame      *self,
+                                              SysprofVisualizerGroupHeader *row,
+                                              GtkListBox                   *list)
+{
+  SysprofVisualizerGroup *group;
+
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (self));
+  g_assert (SYSPROF_IS_VISUALIZER_GROUP_HEADER (row));
+
+  group = g_object_get_data (G_OBJECT (row), "VISUALIZER_GROUP");
+  g_assert (SYSPROF_IS_VISUALIZER_GROUP (group));
+
+  g_signal_emit_by_name (group, "group-activated");
+}
+
+static void
+sysprof_visualizers_frame_finalize (GObject *object)
+{
+  SysprofVisualizersFrame *self = (SysprofVisualizersFrame *)object;
+
+  g_clear_object (&self->selection);
+
+  G_OBJECT_CLASS (sysprof_visualizers_frame_parent_class)->finalize (object);
+}
+
+static void
+sysprof_visualizers_frame_get_property (GObject    *object,
+                                        guint       prop_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+  SysprofVisualizersFrame *self = SYSPROF_VISUALIZERS_FRAME (object);
+
+  switch (prop_id)
+    {
+    case PROP_SELECTED_GROUP:
+      g_value_set_object (value, sysprof_visualizers_frame_get_selected_group (self));
+      break;
+
+    case PROP_SELECTION:
+      g_value_set_object (value, sysprof_visualizers_frame_get_selection (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+sysprof_visualizers_frame_class_init (SysprofVisualizersFrameClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->finalize = sysprof_visualizers_frame_finalize;
+  object_class->get_property = sysprof_visualizers_frame_get_property;
+
+  widget_class->size_allocate = sysprof_visualizers_frame_size_allocate;
+
+  container_class->add = sysprof_visualizers_frame_add;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/sysprof/ui/sysprof-visualizers-frame.ui");
+  gtk_widget_class_set_css_name (widget_class, "SysprofVisualizersFrame");
+  gtk_widget_class_bind_template_child (widget_class, SysprofVisualizersFrame, groups);
+  gtk_widget_class_bind_template_child (widget_class, SysprofVisualizersFrame, hscrollbar);
+  gtk_widget_class_bind_template_child (widget_class, SysprofVisualizersFrame, hscroller);
+  gtk_widget_class_bind_template_child (widget_class, SysprofVisualizersFrame, left_column);
+  gtk_widget_class_bind_template_child (widget_class, SysprofVisualizersFrame, ticks);
+  gtk_widget_class_bind_template_child (widget_class, SysprofVisualizersFrame, ticks_scroller);
+  gtk_widget_class_bind_template_child (widget_class, SysprofVisualizersFrame, visualizers);
+  gtk_widget_class_bind_template_child (widget_class, SysprofVisualizersFrame, vscroller);
+  gtk_widget_class_bind_template_child (widget_class, SysprofVisualizersFrame, zoom_manager);
+  gtk_widget_class_bind_template_child (widget_class, SysprofVisualizersFrame, zoom_scale);
+
+  properties [PROP_SELECTED_GROUP] =
+    g_param_spec_object ("selected-group",
+                         "Selected Group",
+                         "The selected group",
+                         SYSPROF_TYPE_VISUALIZER_GROUP,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SELECTION] =
+    g_param_spec_object ("selection",
+                         "Selection",
+                         "The time selection",
+                         SYSPROF_TYPE_SELECTION,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  g_type_ensure (SYSPROF_TYPE_SCROLLMAP);
+  g_type_ensure (SYSPROF_TYPE_VISUALIZER_TICKS);
+  g_type_ensure (SYSPROF_TYPE_ZOOM_MANAGER);
+}
+
+static void
+sysprof_visualizers_frame_init (SysprofVisualizersFrame *self)
+{
+  GtkAdjustment *hadj;
+  GtkAdjustment *zadj;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->selection = g_object_new (SYSPROF_TYPE_SELECTION, NULL);
+
+  zadj = sysprof_zoom_manager_get_adjustment (self->zoom_manager);
+  hadj = gtk_scrolled_window_get_hadjustment (self->hscroller);
+
+  gtk_scrolled_window_set_hadjustment (self->ticks_scroller, hadj);
+  gtk_range_set_adjustment (GTK_RANGE (self->hscrollbar), hadj);
+  gtk_range_set_adjustment (GTK_RANGE (self->zoom_scale), zadj);
+
+  gtk_widget_insert_action_group (GTK_WIDGET (self),
+                                  "zoom",
+                                  G_ACTION_GROUP (self->zoom_manager));
+
+  g_signal_connect_object (self->groups,
+                           "row-activated",
+                           G_CALLBACK (sysprof_visualizers_frame_group_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->selection,
+                           "changed",
+                           G_CALLBACK (sysprof_visualizers_frame_selection_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->visualizers,
+                           "draw",
+                           G_CALLBACK (visualizers_draw_after_cb),
+                           self,
+                           G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+
+  g_signal_connect_object (self->visualizers,
+                           "realize",
+                           G_CALLBACK (visualizers_realize_after_cb),
+                           self,
+                           G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+
+  g_signal_connect_object (self->visualizers,
+                           "button-press-event",
+                           G_CALLBACK (visualizers_button_press_event_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->visualizers,
+                           "button-release-event",
+                           G_CALLBACK (visualizers_button_release_event_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->visualizers,
+                           "motion-notify-event",
+                           G_CALLBACK (visualizers_motion_notify_event_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->zoom_manager,
+                           "notify::zoom",
+                           G_CALLBACK (sysprof_visualizers_frame_notify_zoom),
+                           self,
+                           G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+}
+
+/**
+ * sysprof_visualizers_frame_get_selected_group:
+ *
+ * Gets the currently selected group.
+ *
+ * Returns: (transfer none) (nullable): the selected row
+ *
+ * Since: 3.34
+ */
+SysprofVisualizerGroup *
+sysprof_visualizers_frame_get_selected_group (SysprofVisualizersFrame *self)
+{
+  GtkListBoxRow *row;
+
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZERS_FRAME (self), NULL);
+
+  row = gtk_list_box_get_selected_row (self->groups);
+
+  return SYSPROF_VISUALIZER_GROUP (row);
+}
+
+/**
+ * sysprof_visualizers_frame_get_selection:
+ *
+ * Get the time selection
+ *
+ * Returns: (transfer none): a #SysprofSelection
+ *
+ * Since: 3.34
+ */
+SysprofSelection *
+sysprof_visualizers_frame_get_selection (SysprofVisualizersFrame *self)
+{
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZERS_FRAME (self), NULL);
+
+  return self->selection;
+}
+
+static gint
+compare_gint64 (const gint64 *a,
+                const gint64 *b)
+{
+  if (*a < *b)
+    return -1;
+  else if (*a > *b)
+    return 1;
+  else
+    return 0;
+}
+
+static gboolean
+index_frame_times_frame_cb (const SysprofCaptureFrame *frame,
+                            gpointer                   user_data)
+{
+  GArray *array = user_data;
+
+  /* Track timing, but ignore some common types at startup */
+  if (frame->type != SYSPROF_CAPTURE_FRAME_MAP &&
+      frame->type != SYSPROF_CAPTURE_FRAME_PROCESS)
+    g_array_append_val (array, frame->time);
+
+  return TRUE;
+}
+
+static void
+index_frame_times_worker (GTask        *task,
+                          gpointer      source_object,
+                          gpointer      task_data,
+                          GCancellable *cancellable)
+{
+  SysprofCaptureCursor *cursor = task_data;
+  GArray *timings = NULL;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (SYSPROF_IS_VISUALIZERS_FRAME (source_object));
+  g_assert (cursor != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  timings = g_array_new (FALSE, FALSE, sizeof (gint64));
+  sysprof_capture_cursor_foreach (cursor, index_frame_times_frame_cb, timings);
+  g_array_sort (timings, (GCompareFunc) compare_gint64);
+
+  g_task_return_pointer (task,
+                         g_steal_pointer (&timings),
+                         (GDestroyNotify) g_array_unref);
+}
+
+void
+sysprof_visualizers_frame_load_async (SysprofVisualizersFrame *self,
+                                      SysprofCaptureReader    *reader,
+                                      GCancellable            *cancellable,
+                                      GAsyncReadyCallback      callback,
+                                      gpointer                 user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  GtkAllocation alloc;
+
+  g_return_if_fail (SYSPROF_IS_VISUALIZERS_FRAME (self));
+  g_return_if_fail (reader != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  gtk_widget_get_allocation (GTK_WIDGET (self->ticks), &alloc);
+
+  /* At this point, the SysprofDisplay should have already scanned the
+   * reader for all of the events and therefore we can trust the begin
+   * and end time for the capture.
+   */
+  self->begin_time = sysprof_capture_reader_get_start_time (reader);
+  self->end_time = sysprof_capture_reader_get_end_time (reader);
+
+  if (alloc.width)
+    sysprof_visualizers_frame_apply_zoom (self, &alloc);
+
+   /* Now we need to run through the frames and index their times
+    * so that we can calculate the number of items per bucket when
+    * drawing the scrollbar.
+    */
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, sysprof_visualizers_frame_load_async);
+  g_task_set_task_data (task,
+                        sysprof_capture_cursor_new (reader),
+                        (GDestroyNotify) sysprof_capture_cursor_unref);
+  g_task_run_in_thread (task, index_frame_times_worker);
+}
+
+gboolean
+sysprof_visualizers_frame_load_finish (SysprofVisualizersFrame  *self,
+                                       GAsyncResult             *result,
+                                       GError                  **error)
+{
+  g_autoptr(GArray) timings = NULL;
+
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZERS_FRAME (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  if ((timings = g_task_propagate_pointer (G_TASK (result), error)))
+    {
+      GtkAllocation alloc;
+
+      gtk_widget_get_allocation (GTK_WIDGET (self->ticks), &alloc);
+      sysprof_scrollmap_set_timings (self->hscrollbar, timings);
+      sysprof_visualizers_frame_apply_zoom (self, &alloc);
+      gtk_widget_queue_resize (GTK_WIDGET (self));
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+/**
+ * sysprof_visualizers_frame_get_zoom_manager:
+ *
+ * Gets the zoom manager for the frame.
+ *
+ * Returns: (transfer none): a #SysprofZoomManager
+ */
+SysprofZoomManager *
+sysprof_visualizers_frame_get_zoom_manager (SysprofVisualizersFrame *self)
+{
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZERS_FRAME (self), NULL);
+
+  return self->zoom_manager;
+}
+
+/**
+ * sysprof_visualizers_frame_get_size_group:
+ *
+ * gets the left column size group.
+ *
+ * Returns: (transfer none): a size group
+ */
+GtkSizeGroup *
+sysprof_visualizers_frame_get_size_group (SysprofVisualizersFrame *self)
+{
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZERS_FRAME (self), NULL);
+
+  return self->left_column;
+}
+
+/**
+ * sysprof_visualizers_frame_get_hadjustment:
+ *
+ * Gets the scroll adjustment used for horizontal scrolling
+ *
+ * Returns: (transfer none): a #GtkAdjustment
+ */
+GtkAdjustment *
+sysprof_visualizers_frame_get_hadjustment (SysprofVisualizersFrame *self)
+{
+  g_return_val_if_fail (SYSPROF_IS_VISUALIZERS_FRAME (self), NULL);
+
+  return gtk_range_get_adjustment (GTK_RANGE (self->hscrollbar));
+}
diff --git a/src/libsysprof-ui/sysprof-visualizers-frame.h b/src/libsysprof-ui/sysprof-visualizers-frame.h
new file mode 100644
index 0000000..d0501ce
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-visualizers-frame.h
@@ -0,0 +1,49 @@
+/* sysprof-visualizers-frame.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <sysprof.h>
+
+#include "sysprof-visualizer-group.h"
+#include "sysprof-zoom-manager.h"
+
+G_BEGIN_DECLS
+
+#define SYSPROF_TYPE_VISUALIZERS_FRAME (sysprof_visualizers_frame_get_type())
+
+G_DECLARE_FINAL_TYPE (SysprofVisualizersFrame, sysprof_visualizers_frame, SYSPROF, VISUALIZERS_FRAME, GtkBin)
+
+SysprofSelection       *sysprof_visualizers_frame_get_selection      (SysprofVisualizersFrame  *self);
+SysprofVisualizerGroup *sysprof_visualizers_frame_get_selected_group (SysprofVisualizersFrame  *self);
+SysprofZoomManager     *sysprof_visualizers_frame_get_zoom_manager   (SysprofVisualizersFrame  *self);
+void                    sysprof_visualizers_frame_load_async         (SysprofVisualizersFrame  *self,
+                                                                      SysprofCaptureReader     *reader,
+                                                                      GCancellable             *cancellable,
+                                                                      GAsyncReadyCallback       callback,
+                                                                      gpointer                  user_data);
+gboolean                sysprof_visualizers_frame_load_finish        (SysprofVisualizersFrame  *self,
+                                                                      GAsyncResult             *result,
+                                                                      GError                  **error);
+GtkSizeGroup           *sysprof_visualizers_frame_get_size_group     (SysprofVisualizersFrame  *self);
+GtkAdjustment          *sysprof_visualizers_frame_get_hadjustment    (SysprofVisualizersFrame  *self);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/sysprof-visualizers-frame.ui b/src/libsysprof-ui/sysprof-visualizers-frame.ui
new file mode 100644
index 0000000..11cda09
--- /dev/null
+++ b/src/libsysprof-ui/sysprof-visualizers-frame.ui
@@ -0,0 +1,419 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.0 -->
+<interface>
+  <requires lib="gtk+" version="3.22"/>
+  <template class="SysprofVisualizersFrame" parent="GtkBin">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkBox" id="box1">
+                <property name="width_request">125</property>
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="orientation">vertical</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="orientation">horizontal</property>
+                    <property name="visible">true</property>
+                    <property name="margin-top">3</property>
+                    <property name="margin-bottom">3</property>
+                    <property name="margin-start">7</property>
+                    <property name="margin-end">7</property>
+                    <style>
+                      <class name="left-column"/>
+                    </style>
+                    <child type="center">
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="margin_start">6</property>
+                        <property name="margin_end">6</property>
+                        <property name="margin_top">3</property>
+                        <property name="margin_bottom">3</property>
+                        <property name="label" translatable="yes">Instruments</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton">
+                        <property name="action-name">display.page</property>
+                        <property name="action-target">'details'</property>
+                        <property name="tooltip-text" translatable="yes">Select for more details</property>
+                        <property name="visible">true</property>
+                        <style>
+                          <class name="image-button"/>
+                          <class name="small-button"/>
+                          <class name="flat"/>
+                        </style>
+                        <child>
+                          <object class="GtkImage">
+                            <property name="icon-name">preferences-system-details-symbolic</property>
+                            <property name="pixel-size">16</property>
+                            <property name="visible">true</property>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="pack-type">end</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkSeparator">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkSeparator">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="orientation">vertical</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox" id="ticks_box">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="vexpand">False</property>
+                <property name="orientation">vertical</property>
+                <child>
+                  <object class="GtkScrolledWindow" id="ticks_scroller">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="hexpand">True</property>
+                    <property name="vexpand">True</property>
+                    <property name="hscrollbar_policy">external</property>
+                    <property name="vscrollbar_policy">never</property>
+                    <child>
+                      <object class="GtkViewport">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="shadow_type">none</property>
+                        <child>
+                          <object class="SysprofVisualizerTicks" id="ticks">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkSeparator">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow" id="vscroller">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="vexpand">True</property>
+            <property name="hscrollbar_policy">never</property>
+            <property name="propagate_natural_height">True</property>
+            <child>
+              <object class="GtkViewport">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="shadow_type">none</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="orientation">horizontal</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkListBox" id="groups">
+                        <property name="width_request">125</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="hexpand">False</property>
+                        <property name="selection_mode">browse</property>
+                        <style>
+                          <class name="left-column"/>
+                          <class name="visualizer-groups"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSeparator">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkScrolledWindow" id="hscroller">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="hexpand">True</property>
+                        <property name="vexpand">True</property>
+                        <property name="hscrollbar_policy">external</property>
+                        <property name="vscrollbar_policy">never</property>
+                        <property name="propagate_natural_height">True</property>
+                        <child>
+                          <object class="GtkViewport">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="shadow_type">none</property>
+                            <child>
+                              <object class="GtkListBox" id="visualizers">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="selection_mode">none</property>
+                                <property name="activate_on_single_click">False</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">2</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkSeparator">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <style>
+              <class name="inline-toolbar"/>
+            </style>
+            <child>
+              <object class="GtkBox" id="box2">
+                <property name="width_request">125</property>
+                <property name="visible">True</property>
+                <property name="margin-start">6</property>
+                <property name="margin-end">6</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">False</property>
+                <style>
+                  <class name="left-column"/>
+                </style>
+                <child>
+                  <object class="GtkButton" id="zoom_out">
+                    <property name="action-name">zoom.zoom-out</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="valign">center</property>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="icon_name">zoom-out-symbolic</property>
+                      </object>
+                    </child>
+                    <style>
+                      <class name="image-button"/>
+                      <class name="small-button"/>
+                      <class name="flat"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkScale" id="zoom_scale">
+                    <property name="width_request">175</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="hexpand">True</property>
+                    <property name="round_digits">1</property>
+                    <property name="draw_value">False</property>
+                    <marks>
+                      <mark value="0.0" position="bottom"/>
+                    </marks>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="zoom_in">
+                    <property name="action-name">zoom.zoom-in</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="relief">none</property>
+                    <property name="valign">center</property>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="icon_name">zoom-in-symbolic</property>
+                      </object>
+                    </child>
+                    <style>
+                      <class name="image-button"/>
+                      <class name="small-button"/>
+                      <class name="flat"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkSeparator">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="SysprofScrollmap" id="hscrollbar">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">3</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkSizeGroup" id="left_column">
+    <widgets>
+      <widget name="box1"/>
+      <widget name="groups"/>
+      <widget name="box2"/>
+    </widgets>
+  </object>
+  <object class="GtkSizeGroup" id="top_row">
+    <property name="mode">vertical</property>
+    <widgets>
+      <widget name="box1"/>
+      <widget name="ticks_box"/>
+    </widgets>
+  </object>
+  <object class="GtkSizeGroup" id="row_size_group">
+    <widgets>
+      <widget name="ticks"/>
+      <widget name="visualizers"/>
+    </widgets>
+  </object>
+  <object class="SysprofZoomManager" id="zoom_manager"/>
+</interface>
diff --git a/src/libsysprof-ui/sysprof-zoom-manager.h b/src/libsysprof-ui/sysprof-zoom-manager.h
index 8b84953..d25742f 100644
--- a/src/libsysprof-ui/sysprof-zoom-manager.h
+++ b/src/libsysprof-ui/sysprof-zoom-manager.h
@@ -20,61 +20,38 @@
 
 #pragma once
 
-#if !defined (SYSPROF_UI_INSIDE) && !defined (SYSPROF_UI_COMPILATION)
-# error "Only <sysprof-ui.h> can be included directly."
-#endif
-
-#include <glib-object.h>
-
-#include "sysprof-version-macros.h"
+#include <gtk/gtk.h>
 
 G_BEGIN_DECLS
 
 #define SYSPROF_TYPE_ZOOM_MANAGER (sysprof_zoom_manager_get_type())
 
-SYSPROF_AVAILABLE_IN_ALL
 G_DECLARE_FINAL_TYPE (SysprofZoomManager, sysprof_zoom_manager, SYSPROF, ZOOM_MANAGER, GObject)
 
-SYSPROF_AVAILABLE_IN_ALL
 SysprofZoomManager *sysprof_zoom_manager_new                    (void);
-SYSPROF_AVAILABLE_IN_ALL
+GtkAdjustment      *sysprof_zoom_manager_get_adjustment         (SysprofZoomManager *self);
 gboolean            sysprof_zoom_manager_get_can_zoom_in        (SysprofZoomManager *self);
-SYSPROF_AVAILABLE_IN_ALL
 gboolean            sysprof_zoom_manager_get_can_zoom_out       (SysprofZoomManager *self);
-SYSPROF_AVAILABLE_IN_ALL
 gboolean            sysprof_zoom_manager_get_min_zoom           (SysprofZoomManager *self);
-SYSPROF_AVAILABLE_IN_ALL
 gboolean            sysprof_zoom_manager_get_max_zoom           (SysprofZoomManager *self);
-SYSPROF_AVAILABLE_IN_ALL
 void                sysprof_zoom_manager_set_min_zoom           (SysprofZoomManager *self,
                                                                  gdouble             min_zoom);
-SYSPROF_AVAILABLE_IN_ALL
 void                sysprof_zoom_manager_set_max_zoom           (SysprofZoomManager *self,
                                                                  gdouble             max_zoom);
-SYSPROF_AVAILABLE_IN_ALL
 void                sysprof_zoom_manager_zoom_in                (SysprofZoomManager *self);
-SYSPROF_AVAILABLE_IN_ALL
 void                sysprof_zoom_manager_zoom_out               (SysprofZoomManager *self);
-SYSPROF_AVAILABLE_IN_ALL
 void                sysprof_zoom_manager_reset                  (SysprofZoomManager *self);
-SYSPROF_AVAILABLE_IN_ALL
 gdouble             sysprof_zoom_manager_get_zoom               (SysprofZoomManager *self);
-SYSPROF_AVAILABLE_IN_ALL
 void                sysprof_zoom_manager_set_zoom               (SysprofZoomManager *self,
                                                                  gdouble             zoom);
-SYSPROF_AVAILABLE_IN_ALL
 gchar              *sysprof_zoom_manager_get_zoom_label         (SysprofZoomManager *self);
-SYSPROF_AVAILABLE_IN_ALL
 gint                sysprof_zoom_manager_get_width_for_duration (SysprofZoomManager *self,
                                                                  gint64              duration);
-SYSPROF_AVAILABLE_IN_ALL
 gint64              sysprof_zoom_manager_get_duration_for_width (SysprofZoomManager *self,
                                                                  gint                width);
-SYSPROF_AVAILABLE_IN_ALL
 gdouble             sysprof_zoom_manager_fit_zoom_for_duration  (SysprofZoomManager *self,
                                                                  gint64              duration,
                                                                  gint                width);
-SYSPROF_AVAILABLE_IN_ALL
 gdouble             sysprof_zoom_manager_get_offset_at_time     (SysprofZoomManager *self,
                                                                  gint64              offset,
                                                                  gint                width);
diff --git a/src/tests/meson.build b/src/tests/meson.build
index 7cebc3c..53a3785 100644
--- a/src/tests/meson.build
+++ b/src/tests/meson.build
@@ -61,7 +61,8 @@ if get_option('enable_gtk')
     dependencies: test_ui_deps,
   )
 
-  test_zoom = executable('test-zoom', 'test-zoom.c',
+  test_zoom = executable('test-zoom',
+    ['test-zoom.c', '../libsysprof-ui/sysprof-zoom-manager.c'],
           c_args: test_cflags,
     dependencies: test_ui_deps,
   )
diff --git a/src/tests/test-capture-view.c b/src/tests/test-capture-view.c
index 192b111..1dca19a 100644
--- a/src/tests/test-capture-view.c
+++ b/src/tests/test-capture-view.c
@@ -25,7 +25,7 @@ main (gint argc,
       gchar *argv[])
 {
   GtkWindow *window;
-  SysprofCaptureView *view;
+  SysprofDisplay *view;
   SysprofCaptureReader *reader;
   g_autoptr(GError) error = NULL;
 
@@ -44,16 +44,16 @@ main (gint argc,
     }
 
   window = g_object_new (GTK_TYPE_WINDOW,
-                         "title", "SysprofCaptureView",
+                         "title", "SysprofDisplay",
                          "default-width", 800,
                          "default-height", 600,
                          NULL);
-  view = g_object_new (SYSPROF_TYPE_CAPTURE_VIEW,
+  view = g_object_new (SYSPROF_TYPE_DISPLAY,
                        "visible", TRUE,
                        NULL);
   gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (view));
 
-  sysprof_capture_view_load_async (view, reader, NULL, NULL, NULL);
+  sysprof_display_load_async (view, reader, NULL, NULL, NULL);
 
   g_signal_connect (window, "delete-event", gtk_main_quit, NULL);
   gtk_window_present (GTK_WINDOW (window));


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]