[gnome-builder] rg: add realtime scrolling graph implementation



commit f111bb7928c84b3accd52cf6020481e5aa28bd91
Author: Christian Hergert <christian hergert me>
Date:   Mon Jun 1 16:47:20 2015 -0700

    rg: add realtime scrolling graph implementation
    
    Lightly based on the ideas from writing Uber Graph years ago. However, this
    is much more simplistic in that we don't yet do the ring buffer for the
    backing pixmap. It makes it much simpler to wrap my head around in the
    process (while costing a bit more CPU). Last I checked (a few years ago)
    the pixmap saved an extra 5% CPU depending on the number of data points
    as well size of the graph. At some point, we will probably add this.
    
    The CPU value parser is rather crappy. sscanf() is generally pertty slow,
    so if you feel like optimizing this, go for it. It would be nice to have
    it abstracted to support FreeBSD/OpenBSD/etc as well.
    
    We still need to add scales and other necessary components around the
    graph to allow them to be interacted with. But it's time to get this
    in the primary repository for review.
    
    I assume long term we'll make these visible when we execute the program.
    One of the best things I ever did when developing previous software was
    ensure I watched CPU/Memory/Network graphs while testing and debugging
    code. I'd like to see that here as well.
    
    To begin with, we'll probably make an additional side pane that can be
    enabled/disabled with an easter egg for now to play with various graphs.
    
    To have a go at the graph and test how smooth it can render on your system,
    use the test application in tests/
    
     ./test-cpu-graph -m 1 -s 60
    
    The above command will perform 1 sample per second with a visible range of
    60 seconds on the graph. The Y axis is from 100.0% to 0.0%.

 configure.ac                   |    2 +
 contrib/rg/Makefile.am         |   37 +++
 contrib/rg/rg-column-private.h |   49 ++++
 contrib/rg/rg-column.c         |  305 ++++++++++++++++++++++
 contrib/rg/rg-column.h         |   43 +++
 contrib/rg/rg-cpu-table.c      |  272 +++++++++++++++++++
 contrib/rg/rg-cpu-table.h      |   34 +++
 contrib/rg/rg-graph.c          |  399 ++++++++++++++++++++++++++++
 contrib/rg/rg-graph.h          |   48 ++++
 contrib/rg/rg-line-renderer.c  |  324 +++++++++++++++++++++++
 contrib/rg/rg-line-renderer.h  |   41 +++
 contrib/rg/rg-renderer.c       |   58 ++++
 contrib/rg/rg-renderer.h       |   57 ++++
 contrib/rg/rg-ring.c           |  206 +++++++++++++++
 contrib/rg/rg-ring.h           |   80 ++++++
 contrib/rg/rg-table.c          |  560 ++++++++++++++++++++++++++++++++++++++++
 contrib/rg/rg-table.h          |   73 ++++++
 tests/Makefile.am              |   22 ++-
 tests/test-cpu-graph.c         |  102 ++++++++
 19 files changed, 2711 insertions(+), 1 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 1adb4b6..db20c56 100644
--- a/configure.ac
+++ b/configure.ac
@@ -161,6 +161,7 @@ PKG_CHECK_MODULES(LIBIDE,   [gio-2.0 >= glib_required_version
                              gjs-1.0 >= gjs_required_version
                              gjs-internals-1.0 >= gjs_required_version
                              pygobject-3.0 >= pygobject_required_version])
+PKG_CHECK_MODULES(RG,       [gtk+-3.0 >= gtk_required_version])
 PKG_CHECK_MODULES(SEARCH,   [glib-2.0 >= glib_required_version])
 PKG_CHECK_MODULES(GEDIT,    [glib-2.0 >= glib_required_version
                              gtk+-3.0 >= gtk_required_version])
@@ -366,6 +367,7 @@ AC_CONFIG_FILES([
        contrib/gedit/Makefile
        contrib/libeditorconfig/Makefile
        contrib/nautilus/Makefile
+       contrib/rg/Makefile
        contrib/search/Makefile
        contrib/xml/Makefile
 
diff --git a/contrib/rg/Makefile.am b/contrib/rg/Makefile.am
new file mode 100644
index 0000000..75b01fb
--- /dev/null
+++ b/contrib/rg/Makefile.am
@@ -0,0 +1,37 @@
+noinst_LTLIBRARIES = librg.la
+
+librg_la_SOURCES = \
+       rg-column.c \
+       rg-column.h \
+       rg-column-private.h \
+       rg-cpu-table.c \
+       rg-cpu-table.h \
+       rg-graph.c \
+       rg-graph.h \
+       rg-line-renderer.c \
+       rg-line-renderer.h \
+       rg-renderer.c \
+       rg-renderer.h \
+       rg-ring.c \
+       rg-ring.h \
+       rg-table.c \
+       rg-table.h \
+       $(NULL)
+
+librg_la_CFLAGS = \
+       -I$(top_srcdir)/contrib/egg \
+       $(DEBUG_CFLAGS) \
+       $(OPTIMIZE_CFLAGS) \
+       $(RG_CFLAGS) \
+       $(NULL)
+
+librg_la_LIBADD = \
+       $(RG_LIBS) \
+       $(top_builddir)/contrib/egg/libegg.la \
+       $(NULL)
+
+librg_la_LDFLAGS = \
+       $(OPTIMIZE_LDFLAGS) \
+       $(NULL)
+
+-include $(top_srcdir)/git.mk
diff --git a/contrib/rg/rg-column-private.h b/contrib/rg/rg-column-private.h
new file mode 100644
index 0000000..03f4d34
--- /dev/null
+++ b/contrib/rg/rg-column-private.h
@@ -0,0 +1,49 @@
+/* rg-column-private.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#ifndef RG_COLUMN_PRIVATE_H
+#define RG_COLUMN_PRIVATE_H
+
+#include <glib-object.h>
+
+#include "rg-column.h"
+
+G_BEGIN_DECLS
+
+void  _rg_column_get_value  (RgColumn *self,
+                             guint     index,
+                             GValue   *value);
+void  _rg_column_collect    (RgColumn *self,
+                             guint     index,
+                             va_list   args);
+void  _rg_column_lcopy      (RgColumn *self,
+                             guint     index,
+                             va_list   args);
+void  _rg_column_get        (RgColumn *column,
+                             guint     index,
+                             ...);
+void  _rg_column_set        (RgColumn *column,
+                             guint     index,
+                             ...);
+guint _rg_column_push       (RgColumn *column);
+void  _rg_column_set_n_rows (RgColumn *column,
+                             guint     n_rows);
+
+G_END_DECLS
+
+#endif /* RG_COLUMN_PRIVATE_H */
diff --git a/contrib/rg/rg-column.c b/contrib/rg/rg-column.c
new file mode 100644
index 0000000..795906c
--- /dev/null
+++ b/contrib/rg/rg-column.c
@@ -0,0 +1,305 @@
+/* rg-column.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#include <glib/gi18n.h>
+#include <gobject/gvaluecollector.h>
+
+#include "rg-column.h"
+#include "rg-column-private.h"
+#include "rg-ring.h"
+
+struct _RgColumn
+{
+  GObject  parent_instance;
+  gchar   *name;
+  RgRing  *values;
+  GType    value_type;
+};
+
+G_DEFINE_TYPE (RgColumn, rg_column, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_NAME,
+  PROP_VALUE_TYPE,
+  LAST_PROP
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+
+RgColumn *
+rg_column_new (const gchar *name,
+               GType        value_type)
+{
+  return g_object_new (RG_TYPE_COLUMN,
+                       "name", name,
+                       "value-type", value_type,
+                       NULL);
+}
+
+const gchar *
+rg_column_get_name (RgColumn *self)
+{
+  g_return_val_if_fail (RG_IS_COLUMN (self), NULL);
+
+  return self->name;
+}
+
+void
+rg_column_set_name (RgColumn    *self,
+                    const gchar *name)
+{
+  g_return_if_fail (RG_IS_COLUMN (self));
+
+  if (g_strcmp0 (name, self->name) != 0)
+    {
+      g_free (self->name);
+      self->name = g_strdup (name);
+      g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_NAME]);
+    }
+}
+
+static void
+rg_column_copy_value (gpointer data,
+                      gpointer user_data)
+{
+  const GValue *src_value = data;
+  RgRing *ring = user_data;
+  GValue copy = G_VALUE_INIT;
+
+  if (G_IS_VALUE (src_value))
+    {
+      g_value_init (&copy, G_VALUE_TYPE (src_value));
+      g_value_copy (src_value, &copy);
+    }
+
+  rg_ring_append_val (ring, copy);
+}
+
+void
+_rg_column_set_n_rows (RgColumn *self,
+                       guint     n_rows)
+{
+  RgRing *ring;
+
+  g_return_if_fail (RG_IS_COLUMN (self));
+  g_return_if_fail (n_rows > 0);
+
+  ring = rg_ring_sized_new (sizeof (GValue), n_rows, NULL);
+  rg_ring_foreach (self->values, rg_column_copy_value, ring);
+  g_clear_pointer (&self->values, rg_ring_unref);
+  self->values = ring;
+}
+
+guint
+_rg_column_push (RgColumn *self)
+{
+  GValue value = G_VALUE_INIT;
+  guint ret;
+
+  g_return_val_if_fail (RG_IS_COLUMN (self), 0);
+
+  g_value_init (&value, self->value_type);
+  ret = rg_ring_append_val (self->values, value);
+
+  return ret;
+}
+
+void
+_rg_column_get_value (RgColumn *self,
+                      guint     index,
+                      GValue   *value)
+{
+  const GValue *src_value;
+
+  g_return_if_fail (RG_IS_COLUMN (self));
+  g_return_if_fail (value != NULL);
+  g_return_if_fail (index < self->values->len);
+
+  src_value = &((GValue *)(gpointer)self->values->data)[index];
+
+  g_value_init (value, self->value_type);
+  if (G_IS_VALUE (src_value))
+    g_value_copy (src_value, value);
+}
+
+void
+_rg_column_collect (RgColumn *self,
+                    guint     index,
+                    va_list   args)
+{
+  GValue *value;
+  gchar *errmsg = NULL;
+
+  g_return_if_fail (RG_IS_COLUMN (self));
+  g_return_if_fail (index < self->values->len);
+
+  value = &((GValue *)(gpointer)self->values->data)[index];
+
+  G_VALUE_COLLECT (value, args, 0, &errmsg);
+
+  if (G_UNLIKELY (errmsg != NULL))
+    {
+      g_critical ("%s", errmsg);
+      g_free (errmsg);
+    }
+}
+
+void
+_rg_column_set (RgColumn *self,
+                guint     index,
+                ...)
+{
+  va_list args;
+
+  g_return_if_fail (RG_IS_COLUMN (self));
+  g_return_if_fail (index < self->values->len);
+
+  va_start (args, index);
+  _rg_column_collect (self, index, args);
+  va_end (args);
+}
+
+void
+_rg_column_get (RgColumn *self,
+                guint     index,
+                ...)
+{
+  va_list args;
+
+  g_return_if_fail (RG_IS_COLUMN (self));
+  g_return_if_fail (index < self->values->len);
+
+  va_start (args, index);
+  _rg_column_lcopy (self, index, args);
+  va_end (args);
+}
+
+void
+_rg_column_lcopy (RgColumn *self,
+                  guint     index,
+                  va_list   args)
+{
+  const GValue *value;
+  gchar *errmsg = NULL;
+
+  g_return_if_fail (RG_IS_COLUMN (self));
+  g_return_if_fail (index < self->values->len);
+
+  value = &((GValue *)(gpointer)self->values->data)[index];
+
+  if (!G_IS_VALUE (value))
+    return;
+
+  G_VALUE_LCOPY (value, args, 0, &errmsg);
+
+  if (G_UNLIKELY (errmsg != NULL))
+    {
+      g_critical ("%s", errmsg);
+      g_free (errmsg);
+    }
+}
+
+static void
+rg_column_finalize (GObject *object)
+{
+  RgColumn *self = (RgColumn *)object;
+
+  g_clear_pointer (&self->name, g_free);
+  g_clear_pointer (&self->values, rg_ring_unref);
+
+  G_OBJECT_CLASS (rg_column_parent_class)->finalize (object);
+}
+
+static void
+rg_column_get_property (GObject    *object,
+                        guint       prop_id,
+                        GValue     *value,
+                        GParamSpec *pspec)
+{
+  RgColumn *self = RG_COLUMN (object);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      g_value_set_string (value, rg_column_get_name (self));
+      break;
+
+    case PROP_VALUE_TYPE:
+      g_value_set_gtype (value, self->value_type);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+rg_column_set_property (GObject      *object,
+                        guint         prop_id,
+                        const GValue *value,
+                        GParamSpec   *pspec)
+{
+  RgColumn *self = RG_COLUMN (object);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      rg_column_set_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_VALUE_TYPE:
+      self->value_type = g_value_get_gtype (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+rg_column_class_init (RgColumnClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = rg_column_finalize;
+  object_class->get_property = rg_column_get_property;
+  object_class->set_property = rg_column_set_property;
+
+  gParamSpecs [PROP_NAME] =
+    g_param_spec_string ("name",
+                         _("Name"),
+                         _("The name of the column"),
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_VALUE_TYPE] =
+    g_param_spec_gtype ("value-type",
+                        _("Value Type"),
+                        _("Value Type"),
+                        G_TYPE_NONE,
+                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
+}
+
+static void
+rg_column_init (RgColumn *self)
+{
+  self->values = rg_ring_sized_new (sizeof (GValue), 60, (GDestroyNotify)g_value_unset);
+}
diff --git a/contrib/rg/rg-column.h b/contrib/rg/rg-column.h
new file mode 100644
index 0000000..65d581a
--- /dev/null
+++ b/contrib/rg/rg-column.h
@@ -0,0 +1,43 @@
+/* rg-column.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#ifndef RG_COLUMN_H
+#define RG_COLUMN_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define RG_TYPE_COLUMN (rg_column_get_type())
+
+G_DECLARE_FINAL_TYPE (RgColumn, rg_column, RG, COLUMN, GObject)
+
+struct _RgColumnClass
+{
+  GObjectClass parent;
+};
+
+RgColumn    *rg_column_new      (const gchar *name,
+                                 GType        value_type);
+const gchar *rg_column_get_name (RgColumn    *self);
+void         rg_column_set_name (RgColumn    *self,
+                                 const gchar *name);
+
+G_END_DECLS
+
+#endif /* RG_COLUMN_H */
diff --git a/contrib/rg/rg-cpu-table.c b/contrib/rg/rg-cpu-table.c
new file mode 100644
index 0000000..13134c1
--- /dev/null
+++ b/contrib/rg/rg-cpu-table.c
@@ -0,0 +1,272 @@
+/* rg-cpu-table.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#ifndef __linux__
+# error "This file only supports Linux."
+#endif
+
+#include <ctype.h>
+#include <stdio.h>
+
+#include "rg-cpu-table.h"
+
+typedef struct
+{
+  gdouble total;
+  gdouble freq;
+  glong   last_user;
+  glong   last_idle;
+  glong   last_system;
+  glong   last_nice;
+  glong   last_iowait;
+  glong   last_irq;
+  glong   last_softirq;
+  glong   last_steal;
+  glong   last_guest;
+  glong   last_guest_nice;
+} CpuInfo;
+
+struct _RgCpuTable
+{
+  RgTable  parent_instance;
+
+  GArray  *cpu_info;
+  guint    n_cpu;
+
+  guint    poll_source;
+  guint    poll_interval_msec;
+};
+
+G_DEFINE_TYPE (RgCpuTable, rg_cpu_table, RG_TYPE_TABLE)
+
+#ifdef __linux__
+static void
+rg_cpu_table_poll (RgCpuTable *self)
+{
+  gchar cpu[64] = { 0 };
+  glong user;
+  glong sys;
+  glong nice;
+  glong idle;
+  glong iowait;
+  glong irq;
+  glong softirq;
+  glong steal;
+  glong guest;
+  glong guest_nice;
+  glong user_calc;
+  glong system_calc;
+  glong nice_calc;
+  glong idle_calc;
+  glong iowait_calc;
+  glong irq_calc;
+  glong softirq_calc;
+  glong steal_calc;
+  glong guest_calc;
+  glong guest_nice_calc;
+  gchar *buf = NULL;
+  glong total;
+  gchar *line;
+  gint ret;
+  gint id;
+  gint i;
+
+  if (g_file_get_contents("/proc/stat", &buf, NULL, NULL))
+    {
+      line = buf;
+      for (i = 0; buf[i]; i++)
+        {
+          if (buf[i] == '\n') {
+            buf[i] = '\0';
+            if (g_str_has_prefix(line, "cpu"))
+              {
+                if (isdigit(line[3]))
+                  {
+                    CpuInfo *cpu_info;
+
+                    user = nice = sys = idle = id = 0;
+                    ret = sscanf (line, "%s %ld %ld %ld %ld %ld %ld %ld %ld %ld %ld",
+                                  cpu, &user, &nice, &sys, &idle,
+                                  &iowait, &irq, &softirq, &steal, &guest, &guest_nice);
+                    if (ret != 11)
+                      goto next;
+
+                    ret = sscanf(cpu, "cpu%d", &id);
+
+                    if (ret != 1 || id < 0 || id >= self->n_cpu)
+                      goto next;
+
+                    cpu_info = &g_array_index (self->cpu_info, CpuInfo, id);
+
+                    user_calc = user - cpu_info->last_user;
+                    nice_calc = nice - cpu_info->last_nice;
+                    system_calc = sys - cpu_info->last_system;
+                    idle_calc = idle - cpu_info->last_idle;
+                    iowait_calc = iowait - cpu_info->last_iowait;
+                    irq_calc = irq - cpu_info->last_irq;
+                    softirq_calc = softirq - cpu_info->last_softirq;
+                    steal_calc = steal - cpu_info->last_steal;
+                    guest_calc = guest - cpu_info->last_guest;
+                    guest_nice_calc = guest_nice - cpu_info->last_guest_nice;
+
+                    total = user_calc + nice_calc + system_calc + idle_calc + iowait_calc + irq_calc + 
softirq_calc + steal_calc + guest_calc + guest_nice_calc;
+                    cpu_info->total = ((total - idle_calc) / (gdouble)total) * 100.0;
+
+                    cpu_info->last_user = user;
+                    cpu_info->last_nice = nice;
+                    cpu_info->last_idle = idle;
+                    cpu_info->last_system = sys;
+                    cpu_info->last_iowait = iowait;
+                    cpu_info->last_irq = irq;
+                    cpu_info->last_softirq = softirq;
+                    cpu_info->last_steal = steal;
+                    cpu_info->last_guest = guest;
+                    cpu_info->last_guest_nice = guest_nice;
+                  }
+              } else {
+                /* CPU info comes first. Skip further lines. */
+                break;
+              }
+
+          next:
+            line = &buf[i + 1];
+          }
+      }
+  }
+
+  g_free (buf);
+}
+#else
+static void
+rg_cpu_table_poll (RgCpuTable *self)
+{
+  /*
+   * TODO: calculate cpu info for FreeBSD/OpenBSD/etc.
+   *
+   * While we are at it, we should make the Linux code above non-shitty.
+   */
+}
+#endif
+
+static gboolean
+rg_cpu_table_poll_cb (gpointer user_data)
+{
+  RgCpuTable *self = user_data;
+  RgTableIter iter;
+  guint i;
+
+  rg_cpu_table_poll (self);
+
+  rg_table_push (RG_TABLE (self), &iter, g_get_monotonic_time ());
+
+  for (i = 0; i < self->cpu_info->len; i++)
+    {
+      CpuInfo *cpu_info;
+
+      cpu_info = &g_array_index (self->cpu_info, CpuInfo, i);
+      rg_table_iter_set (&iter, i, cpu_info->total, -1);
+    }
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+rg_cpu_table_constructed (GObject *object)
+{
+  RgCpuTable *self = (RgCpuTable *)object;
+  gint64 timespan;
+  guint max_samples;
+  guint i;
+
+  G_OBJECT_CLASS (rg_cpu_table_parent_class)->constructed (object);
+
+  max_samples = rg_table_get_max_samples (RG_TABLE (self));
+  timespan = rg_table_get_timespan (RG_TABLE (self));
+
+  self->poll_interval_msec = (gdouble)timespan / (gdouble)(max_samples - 1) / 1000L;
+
+  if (self->poll_interval_msec == 0)
+    {
+      g_critical ("Implausible timespan/max_samples combination for graph.");
+      self->poll_interval_msec = 1000;
+    }
+
+  self->n_cpu = g_get_num_processors ();
+
+  for (i = 0; i < self->n_cpu; i++)
+    {
+      CpuInfo cpu_info = { 0 };
+      RgColumn *column;
+      gchar *name;
+
+      name = g_strdup_printf ("CPU %d", i + 1);
+      column = rg_column_new (name, G_TYPE_DOUBLE);
+
+      rg_table_add_column (RG_TABLE (self), column);
+      g_array_append_val (self->cpu_info, cpu_info);
+
+      g_object_unref (column);
+      g_free (name);
+    }
+
+  rg_cpu_table_poll (self);
+
+  self->poll_source = g_timeout_add (self->poll_interval_msec, rg_cpu_table_poll_cb, self);
+}
+
+static void
+rg_cpu_table_finalize (GObject *object)
+{
+  RgCpuTable *self = (RgCpuTable *)object;
+
+  if (self->poll_source != 0)
+    {
+      g_source_remove (self->poll_source);
+      self->poll_source = 0;
+    }
+
+  g_clear_pointer (&self->cpu_info, g_array_unref);
+
+  G_OBJECT_CLASS (rg_cpu_table_parent_class)->finalize (object);
+}
+
+static void
+rg_cpu_table_class_init (RgCpuTableClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = rg_cpu_table_constructed;
+  object_class->finalize = rg_cpu_table_finalize;
+}
+
+static void
+rg_cpu_table_init (RgCpuTable *self)
+{
+  self->cpu_info = g_array_new (FALSE, FALSE, sizeof (CpuInfo));
+
+  g_object_set (self,
+                "value-min", 0.0,
+                "value-max", 100.0,
+                NULL);
+}
+
+RgTable *
+rg_cpu_table_new (void)
+{
+  return g_object_new (RG_TYPE_CPU_TABLE, NULL);
+}
diff --git a/contrib/rg/rg-cpu-table.h b/contrib/rg/rg-cpu-table.h
new file mode 100644
index 0000000..b723d7f
--- /dev/null
+++ b/contrib/rg/rg-cpu-table.h
@@ -0,0 +1,34 @@
+/* rg-cpu-table.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#ifndef RG_CPU_TABLE_H
+#define RG_CPU_TABLE_H
+
+#include "rg-table.h"
+
+G_BEGIN_DECLS
+
+#define RG_TYPE_CPU_TABLE (rg_cpu_table_get_type())
+
+G_DECLARE_FINAL_TYPE (RgCpuTable, rg_cpu_table, RG, CPU_TABLE, RgTable)
+
+RgTable *rg_cpu_table_new (void);
+
+G_END_DECLS
+
+#endif /* RG_CPU_TABLE_H */
diff --git a/contrib/rg/rg-graph.c b/contrib/rg/rg-graph.c
new file mode 100644
index 0000000..6578f48
--- /dev/null
+++ b/contrib/rg/rg-graph.c
@@ -0,0 +1,399 @@
+/* rg-graph.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "egg-signal-group.h"
+
+#include "rg-graph.h"
+
+typedef struct
+{
+  RgTable         *table;
+  EggSignalGroup  *table_signals;
+  GPtrArray       *renderers;
+  cairo_surface_t *surface;
+  guint            tick_handler;
+  gint             x_offset;
+  guint            surface_dirty : 1;
+} RgGraphPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (RgGraph, rg_graph, GTK_TYPE_DRAWING_AREA)
+
+enum {
+  PROP_0,
+  PROP_TABLE,
+  LAST_PROP
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+
+GtkWidget *
+rg_graph_new (void)
+{
+  return g_object_new (RG_TYPE_GRAPH, NULL);
+}
+
+static void
+rg_graph_clear_surface (RgGraph *self)
+{
+  RgGraphPrivate *priv = rg_graph_get_instance_private (self);
+
+  g_assert (RG_IS_GRAPH (self));
+
+  priv->surface_dirty = TRUE;
+}
+
+/**
+ * rg_graph_get_table:
+ *
+ * Gets the #RgGraph:table property.
+ *
+ * Returns: (transfer none) (nullable): An #RgTable or %NULL.
+ */
+RgTable *
+rg_graph_get_table (RgGraph *self)
+{
+  RgGraphPrivate *priv = rg_graph_get_instance_private (self);
+
+  g_return_val_if_fail (RG_IS_GRAPH (self), NULL);
+
+  return priv->table;
+}
+
+void
+rg_graph_set_table (RgGraph *self,
+                    RgTable *table)
+{
+  RgGraphPrivate *priv = rg_graph_get_instance_private (self);
+
+  g_return_if_fail (RG_IS_GRAPH (self));
+  g_return_if_fail (!table || RG_IS_TABLE (table));
+
+  if (g_set_object (&priv->table, table))
+    {
+      egg_signal_group_set_target (priv->table_signals, table);
+      gtk_widget_queue_resize (GTK_WIDGET (self));
+      g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_TABLE]);
+    }
+}
+
+void
+rg_graph_add_renderer (RgGraph    *self,
+                       RgRenderer *renderer)
+{
+  RgGraphPrivate *priv = rg_graph_get_instance_private (self);
+
+  g_return_if_fail (RG_IS_GRAPH (self));
+  g_return_if_fail (RG_IS_RENDERER (renderer));
+
+  g_ptr_array_add (priv->renderers, g_object_ref (renderer));
+  rg_graph_clear_surface (self);
+}
+
+static gboolean
+rg_graph_tick_cb (GtkWidget     *widget,
+                  GdkFrameClock *frame_clock,
+                  gpointer       user_data)
+{
+  RgGraph *self = (RgGraph *)widget;
+  RgGraphPrivate *priv = rg_graph_get_instance_private (self);
+  GtkAllocation alloc;
+  gint64 frame_time;
+  gint64 end_time;
+  gint64 timespan;
+
+  g_assert (RG_IS_GRAPH (self));
+
+  if ((priv->surface == NULL) || (priv->table == NULL))
+    {
+      if (priv->tick_handler != 0)
+        {
+          gtk_widget_remove_tick_callback (widget, priv->tick_handler);
+          priv->tick_handler = 0;
+        }
+
+      return G_SOURCE_REMOVE;
+    }
+
+  gtk_widget_get_allocation (widget, &alloc);
+
+  frame_time = gdk_frame_clock_get_frame_time (frame_clock);
+  end_time = rg_table_get_end_time (priv->table);
+  timespan = rg_table_get_timespan (priv->table);
+
+  priv->x_offset = -((frame_time - end_time) / (gdouble)timespan * alloc.width);
+
+  gtk_widget_queue_draw (widget);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+rg_graph_ensure_surface (RgGraph *self)
+{
+  RgGraphPrivate *priv = rg_graph_get_instance_private (self);
+  GtkAllocation alloc;
+  RgTableIter iter;
+  gint64 begin_time;
+  gint64 end_time;
+  gdouble y_begin;
+  gdouble y_end;
+  cairo_t *cr;
+  gsize i;
+
+  g_assert (RG_IS_GRAPH (self));
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  if (priv->surface == NULL)
+    {
+      priv->surface_dirty = TRUE;
+      priv->surface = gdk_window_create_similar_surface (gtk_widget_get_window (GTK_WIDGET (self)),
+                                                         CAIRO_CONTENT_COLOR_ALPHA,
+                                                         alloc.width,
+                                                         alloc.height);
+    }
+
+  if (priv->table == NULL)
+    return;
+
+  if (priv->surface_dirty)
+    {
+      priv->surface_dirty = FALSE;
+
+      cr = cairo_create (priv->surface);
+
+      cairo_save (cr);
+      gdk_cairo_rectangle (cr, &alloc);
+      cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
+      cairo_fill (cr);
+      cairo_restore (cr);
+
+      g_object_get (priv->table,
+                    "value-min", &y_begin,
+                    "value-max", &y_end,
+                    NULL);
+
+      rg_table_get_iter_last (priv->table, &iter);
+      end_time = rg_table_iter_get_timestamp (&iter);
+      begin_time = end_time - rg_table_get_timespan (priv->table);
+
+      for (i = 0; i < priv->renderers->len; i++)
+        {
+          RgRenderer *renderer;
+
+          renderer = g_ptr_array_index (priv->renderers, i);
+
+          cairo_save (cr);
+          rg_renderer_render (renderer, priv->table, begin_time, end_time, y_begin, y_end, cr, &alloc);
+          cairo_restore (cr);
+        }
+
+      cairo_destroy (cr);
+
+      priv->x_offset = 0;
+    }
+
+  if (priv->tick_handler == 0)
+    priv->tick_handler = gtk_widget_add_tick_callback (GTK_WIDGET (self),
+                                                       rg_graph_tick_cb,
+                                                       self,
+                                                       NULL);
+}
+
+static gboolean
+rg_graph_draw (GtkWidget *widget,
+               cairo_t   *cr)
+{
+  RgGraph *self = (RgGraph *)widget;
+  RgGraphPrivate *priv = rg_graph_get_instance_private (self);
+  GtkStyleContext *style_context;
+  GtkAllocation alloc;
+
+  g_assert (RG_IS_GRAPH (self));
+
+  gtk_widget_get_allocation (widget, &alloc);
+
+  style_context = gtk_widget_get_style_context (widget);
+
+  rg_graph_ensure_surface (self);
+
+  gtk_style_context_save (style_context);
+  gtk_style_context_add_class (style_context, "view");
+  gtk_render_background (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height);
+  gtk_style_context_restore (style_context);
+
+  cairo_save (cr);
+  cairo_set_source_surface (cr, priv->surface, priv->x_offset, 0);
+  gdk_cairo_rectangle (cr, &alloc);
+  cairo_fill (cr);
+  cairo_restore (cr);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+rg_graph_size_allocate (GtkWidget     *widget,
+                        GtkAllocation *alloc)
+{
+  RgGraph *self = (RgGraph *)widget;
+  RgGraphPrivate *priv = rg_graph_get_instance_private (self);
+
+  g_assert (RG_IS_GRAPH (self));
+  g_assert (alloc != NULL);
+
+  g_clear_pointer (&priv->surface, cairo_surface_destroy);
+
+  GTK_WIDGET_CLASS (rg_graph_parent_class)->size_allocate (widget, alloc);
+}
+
+static void
+rg_graph__table_changed (RgGraph *self,
+                         RgTable *table)
+{
+  g_assert (RG_IS_GRAPH (self));
+  g_assert (RG_IS_TABLE (table));
+
+  rg_graph_clear_surface (self);
+}
+
+static void
+rg_graph_destroy (GtkWidget *widget)
+{
+  RgGraph *self = (RgGraph *)widget;
+  RgGraphPrivate *priv = rg_graph_get_instance_private (self);
+
+  if (priv->tick_handler != 0)
+    {
+      gtk_widget_remove_tick_callback (widget, priv->tick_handler);
+      priv->tick_handler = 0;
+    }
+
+  GTK_WIDGET_CLASS (rg_graph_parent_class)->destroy (widget);
+}
+
+static void
+rg_graph_finalize (GObject *object)
+{
+  RgGraph *self = (RgGraph *)object;
+  RgGraphPrivate *priv = rg_graph_get_instance_private (self);
+
+  g_clear_object (&priv->table);
+  g_clear_object (&priv->table_signals);
+  g_clear_pointer (&priv->surface, cairo_surface_destroy);
+  g_clear_pointer (&priv->renderers, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (rg_graph_parent_class)->finalize (object);
+}
+
+static void
+rg_graph_get_property (GObject    *object,
+                       guint       prop_id,
+                       GValue     *value,
+                       GParamSpec *pspec)
+{
+  RgGraph *self = RG_GRAPH (object);
+
+  switch (prop_id)
+    {
+    case PROP_TABLE:
+      g_value_set_object (value, rg_graph_get_table (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+rg_graph_set_property (GObject      *object,
+                       guint         prop_id,
+                       const GValue *value,
+                       GParamSpec   *pspec)
+{
+  RgGraph *self = RG_GRAPH (object);
+
+  switch (prop_id)
+    {
+    case PROP_TABLE:
+      rg_graph_set_table (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+rg_graph_class_init (RgGraphClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = rg_graph_finalize;
+  object_class->get_property = rg_graph_get_property;
+  object_class->set_property = rg_graph_set_property;
+
+  widget_class->destroy = rg_graph_destroy;
+  widget_class->draw = rg_graph_draw;
+  widget_class->size_allocate = rg_graph_size_allocate;
+
+  gParamSpecs [PROP_TABLE] =
+    g_param_spec_object ("table",
+                         _("Table"),
+                         _("The data table for the graph."),
+                         RG_TYPE_TABLE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
+}
+
+static void
+rg_graph_init (RgGraph *self)
+{
+  RgGraphPrivate *priv = rg_graph_get_instance_private (self);
+
+  priv->renderers = g_ptr_array_new_with_free_func (g_object_unref);
+
+  priv->table_signals = egg_signal_group_new (RG_TYPE_TABLE);
+
+  egg_signal_group_connect_object (priv->table_signals,
+                                   "notify::value-max",
+                                   G_CALLBACK (gtk_widget_queue_resize),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  egg_signal_group_connect_object (priv->table_signals,
+                                   "notify::value-min",
+                                   G_CALLBACK (gtk_widget_queue_resize),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  egg_signal_group_connect_object (priv->table_signals,
+                                   "notify::timespan",
+                                   G_CALLBACK (gtk_widget_queue_resize),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  egg_signal_group_connect_object (priv->table_signals,
+                                   "changed",
+                                   G_CALLBACK (rg_graph__table_changed),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+}
diff --git a/contrib/rg/rg-graph.h b/contrib/rg/rg-graph.h
new file mode 100644
index 0000000..b8884a6
--- /dev/null
+++ b/contrib/rg/rg-graph.h
@@ -0,0 +1,48 @@
+/* rg-graph.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#ifndef RG_GRAPH_H
+#define RG_GRAPH_H
+
+#include <gtk/gtk.h>
+
+#include "rg-table.h"
+#include "rg-renderer.h"
+
+G_BEGIN_DECLS
+
+#define RG_TYPE_GRAPH (rg_graph_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (RgGraph, rg_graph, RG, GRAPH, GtkDrawingArea)
+
+struct _RgGraphClass
+{
+  GtkDrawingAreaClass parent_class;
+  gpointer padding[8];
+};
+
+GtkWidget *rg_graph_new          (void);
+void       rg_graph_set_table    (RgGraph *self,
+                                  RgTable *table);
+RgTable   *rg_graph_get_table    (RgGraph *self);
+void       rg_graph_add_renderer (RgGraph    *self,
+                                  RgRenderer *renderer);
+
+G_END_DECLS
+
+#endif /* RG_GRAPH_H */
diff --git a/contrib/rg/rg-line-renderer.c b/contrib/rg/rg-line-renderer.c
new file mode 100644
index 0000000..fed57df
--- /dev/null
+++ b/contrib/rg/rg-line-renderer.c
@@ -0,0 +1,324 @@
+/* rg-line-renderer.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#include <glib/gi18n.h>
+#include <math.h>
+
+#include "rg-line-renderer.h"
+
+struct _RgLineRenderer
+{
+  GObject parent_instance;
+
+  GdkRGBA stroke_color;
+  gdouble line_width;
+  guint   column;
+};
+
+static void rg_line_renderer_init_renderer (RgRendererInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (RgLineRenderer, rg_line_renderer, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (RG_TYPE_RENDERER,
+                                                rg_line_renderer_init_renderer))
+
+enum {
+  PROP_0,
+  PROP_COLUMN,
+  PROP_LINE_WIDTH,
+  PROP_STROKE_COLOR,
+  PROP_STROKE_COLOR_RGBA,
+  LAST_PROP
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+
+RgLineRenderer *
+rg_line_renderer_new (void)
+{
+  return g_object_new (RG_TYPE_LINE_RENDERER, NULL);
+}
+
+static gdouble
+calc_x (RgTableIter *iter,
+        gint64       begin,
+        gint64       end,
+        guint        width)
+{
+  gint64 timestamp;
+
+  timestamp = rg_table_iter_get_timestamp (iter);
+
+  g_assert_cmpint (timestamp, !=, 0);
+
+  return ((timestamp - begin) / (gdouble)(end - begin) * width);
+}
+
+static gdouble
+calc_y (RgTableIter *iter,
+        gdouble      range_begin,
+        gdouble      range_end,
+        guint        height,
+        guint        column)
+{
+  GValue value = G_VALUE_INIT;
+  gdouble y;
+
+  rg_table_iter_get_value (iter, column, &value);
+
+  switch (G_VALUE_TYPE (&value))
+    {
+    case G_TYPE_DOUBLE:
+      y = g_value_get_double (&value);
+      break;
+
+    case G_TYPE_UINT:
+      y = g_value_get_uint (&value);
+      break;
+
+    case G_TYPE_UINT64:
+      y = g_value_get_uint64 (&value);
+      break;
+
+    case G_TYPE_INT:
+      y = g_value_get_int (&value);
+      break;
+
+    case G_TYPE_INT64:
+      y = g_value_get_int64 (&value);
+      break;
+
+    default:
+      y = 0.0;
+      break;
+    }
+
+  y -= range_begin;
+  y /= (range_end - range_begin);
+  y = height - (y * height);
+
+  return y;
+}
+
+static void
+rg_line_renderer_render (RgRenderer                  *renderer,
+                         RgTable                     *table,
+                         gint64                       x_begin,
+                         gint64                       x_end,
+                         gdouble                      y_begin,
+                         gdouble                      y_end,
+                         cairo_t                     *cr,
+                         const cairo_rectangle_int_t *area)
+{
+  RgLineRenderer *self = (RgLineRenderer *)renderer;
+  RgTableIter iter;
+
+  g_assert (RG_IS_LINE_RENDERER (self));
+
+  cairo_save (cr);
+
+  if (rg_table_get_iter_first (table, &iter))
+    {
+      guint max_samples;
+      gdouble chunk;
+      gdouble last_x;
+      gdouble last_y;
+
+      max_samples = rg_table_get_max_samples (table);
+
+      chunk = area->width / (gdouble)(max_samples - 1) / 2.0;
+
+      last_x = calc_x (&iter, x_begin, x_end, area->width);
+      last_y = calc_y (&iter, y_begin, y_end, area->height, self->column);
+
+      cairo_move_to (cr, last_x, last_y);
+
+      while (rg_table_iter_next (&iter))
+        {
+          gdouble x;
+          gdouble y;
+
+          x = calc_x (&iter, x_begin, x_end, area->width);
+          y = calc_y (&iter, y_begin, y_end, area->height, self->column);
+
+          cairo_curve_to (cr,
+                          last_x + chunk,
+                          last_y,
+                          last_x + chunk,
+                          y,
+                          x,
+                          y);
+
+          last_x = x;
+          last_y = y;
+        }
+    }
+
+  cairo_set_line_width (cr, self->line_width);
+  gdk_cairo_set_source_rgba (cr, &self->stroke_color);
+  cairo_stroke (cr);
+
+  cairo_restore (cr);
+}
+
+static void
+rg_line_renderer_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  RgLineRenderer *self = RG_LINE_RENDERER (object);
+
+  switch (prop_id)
+    {
+    case PROP_COLUMN:
+      g_value_set_uint (value, self->column);
+      break;
+
+    case PROP_LINE_WIDTH:
+      g_value_set_double (value, self->line_width);
+      break;
+
+    case PROP_STROKE_COLOR:
+      g_value_take_string (value, gdk_rgba_to_string (&self->stroke_color));
+      break;
+
+    case PROP_STROKE_COLOR_RGBA:
+      g_value_set_boxed (value, &self->stroke_color);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+rg_line_renderer_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  RgLineRenderer *self = RG_LINE_RENDERER (object);
+
+  switch (prop_id)
+    {
+    case PROP_COLUMN:
+      self->column = g_value_get_uint (value);
+      break;
+
+    case PROP_LINE_WIDTH:
+      self->line_width = g_value_get_double (value);
+      break;
+
+    case PROP_STROKE_COLOR:
+      rg_line_renderer_set_stroke_color (self, g_value_get_string (value));
+      break;
+
+    case PROP_STROKE_COLOR_RGBA:
+      rg_line_renderer_set_stroke_color_rgba (self, g_value_get_boxed (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+rg_line_renderer_class_init (RgLineRendererClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = rg_line_renderer_get_property;
+  object_class->set_property = rg_line_renderer_set_property;
+
+  gParamSpecs [PROP_COLUMN] =
+    g_param_spec_uint ("column",
+                       _("Column"),
+                       _("Column"),
+                       0, G_MAXUINT,
+                       0,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_LINE_WIDTH] =
+    g_param_spec_double ("line-width",
+                         _("Line Width"),
+                         _("Line Width"),
+                         0.0, G_MAXDOUBLE,
+                         1.0,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_STROKE_COLOR] =
+    g_param_spec_string ("stroke-color",
+                         _("Stroke Color"),
+                         _("Stroke Color"),
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_STROKE_COLOR_RGBA] =
+    g_param_spec_boxed ("stroke-color-rgba",
+                        _("Stroke Color Rgba"),
+                        _("Stroke Color Rgba"),
+                        GDK_TYPE_RGBA,
+                        (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
+}
+
+static void
+rg_line_renderer_init (RgLineRenderer *self)
+{
+  self->line_width = 1.0;
+}
+
+static void
+rg_line_renderer_init_renderer (RgRendererInterface *iface)
+{
+  iface->render = rg_line_renderer_render;
+}
+
+void
+rg_line_renderer_set_stroke_color_rgba (RgLineRenderer *self,
+                                        const GdkRGBA  *rgba)
+{
+  const GdkRGBA black = { 0, 0, 0, 1.0 };
+
+  g_return_if_fail (RG_IS_LINE_RENDERER (self));
+
+  if (rgba == NULL)
+    rgba = &black;
+
+  if (!gdk_rgba_equal (rgba, &self->stroke_color))
+    {
+      self->stroke_color = *rgba;
+      g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_STROKE_COLOR_RGBA]);
+    }
+}
+
+void
+rg_line_renderer_set_stroke_color (RgLineRenderer *self,
+                                   const gchar    *stroke_color)
+{
+  GdkRGBA rgba;
+
+  g_return_if_fail (RG_IS_LINE_RENDERER (self));
+
+  if (stroke_color == NULL)
+    stroke_color = "#000000";
+
+  if (gdk_rgba_parse (&rgba, stroke_color))
+    rg_line_renderer_set_stroke_color_rgba (self, &rgba);
+}
diff --git a/contrib/rg/rg-line-renderer.h b/contrib/rg/rg-line-renderer.h
new file mode 100644
index 0000000..68cf7fd
--- /dev/null
+++ b/contrib/rg/rg-line-renderer.h
@@ -0,0 +1,41 @@
+/* rg-line-renderer.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#ifndef RG_LINE_RENDERER_H
+#define RG_LINE_RENDERER_H
+
+#include <gdk/gdk.h>
+
+#include "rg-renderer.h"
+
+G_BEGIN_DECLS
+
+#define RG_TYPE_LINE_RENDERER (rg_line_renderer_get_type())
+
+G_DECLARE_FINAL_TYPE (RgLineRenderer, rg_line_renderer, RG, LINE_RENDERER, GObject)
+
+RgLineRenderer *rg_line_renderer_new (void);
+void            rg_line_renderer_set_stroke_color      (RgLineRenderer *self,
+                                                        const gchar    *stroke_color);
+void            rg_line_renderer_set_stroke_color_rgba (RgLineRenderer *self,
+                                                        const GdkRGBA  *stroke_color_rgba);
+const GdkRGBA  *rg_line_renderer_get_stroke_color_rgba (RgLineRenderer *self);
+
+G_END_DECLS
+
+#endif /* RG_LINE_RENDERER_H */
diff --git a/contrib/rg/rg-renderer.c b/contrib/rg/rg-renderer.c
new file mode 100644
index 0000000..7b5e97a
--- /dev/null
+++ b/contrib/rg/rg-renderer.c
@@ -0,0 +1,58 @@
+/* rg-renderer.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#include <cairo/cairo.h>
+
+#include "rg-renderer.h"
+
+G_DEFINE_INTERFACE (RgRenderer, rg_renderer, G_TYPE_OBJECT)
+
+static void
+dummy_render (RgRenderer                  *renderer,
+              RgTable                     *table,
+              gint64                       x_begin,
+              gint64                       x_end,
+              gdouble                      y_begin,
+              gdouble                      y_end,
+              cairo_t                     *cr,
+              const cairo_rectangle_int_t *area)
+{
+}
+
+static void
+rg_renderer_default_init (RgRendererInterface *iface)
+{
+  iface->render = dummy_render;
+}
+
+void
+rg_renderer_render (RgRenderer                  *self,
+                    RgTable                     *table,
+                    gint64                       x_begin,
+                    gint64                       x_end,
+                    gdouble                      y_begin,
+                    gdouble                      y_end,
+                    cairo_t                     *cr,
+                    const cairo_rectangle_int_t *area)
+{
+  g_return_if_fail (RG_IS_RENDERER (self));
+  g_return_if_fail (cr != NULL);
+  g_return_if_fail (area != NULL);
+
+  RG_RENDERER_GET_IFACE (self)->render (self, table, x_begin, x_end, y_begin, y_end, cr, area);
+}
diff --git a/contrib/rg/rg-renderer.h b/contrib/rg/rg-renderer.h
new file mode 100644
index 0000000..91d9c49
--- /dev/null
+++ b/contrib/rg/rg-renderer.h
@@ -0,0 +1,57 @@
+/* rg-renderer.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#ifndef RG_RENDERER_H
+#define RG_RENDERER_H
+
+#include <glib-object.h>
+
+#include "rg-table.h"
+
+G_BEGIN_DECLS
+
+#define RG_TYPE_RENDERER (rg_renderer_get_type ())
+
+G_DECLARE_INTERFACE (RgRenderer, rg_renderer, RG, RENDERER, GObject)
+
+struct _RgRendererInterface
+{
+  GTypeInterface parent;
+
+  void (*render) (RgRenderer                  *self,
+                  RgTable                     *table,
+                  gint64                       x_begin,
+                  gint64                       x_end,
+                  gdouble                      y_begin,
+                  gdouble                      y_end,
+                  cairo_t                     *cr,
+                  const cairo_rectangle_int_t *area);
+};
+
+void rg_renderer_render (RgRenderer                  *self,
+                         RgTable                     *table,
+                         gint64                       x_begin,
+                         gint64                       x_end,
+                         gdouble                      y_begin,
+                         gdouble                      y_end,
+                         cairo_t                     *cr,
+                         const cairo_rectangle_int_t *area);
+
+G_END_DECLS
+
+#endif /* RG_RENDERER_H */
diff --git a/contrib/rg/rg-ring.c b/contrib/rg/rg-ring.c
new file mode 100644
index 0000000..16a3a7d
--- /dev/null
+++ b/contrib/rg/rg-ring.c
@@ -0,0 +1,206 @@
+/* rg-ring.c
+ *
+ * Copyright (C) 2010 Christian Hergert <chris dronelabs com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#define G_LOG_DOMAIN "rg-ring"
+
+#include <string.h>
+
+#include "rg-ring.h"
+
+#define get_element(r,i) ((r)->data + ((r)->elt_size * i))
+
+typedef struct
+{
+  /*< public >*/
+  guint8          *data;      /* Pointer to real data. */
+  guint            len;       /* Length of data allocation. */
+  guint            pos;       /* Position in ring. */
+
+  /*< private >*/
+  guint            elt_size;  /* Size of each element. */
+  gboolean         looped;    /* Have we wrapped around at least once. */
+  GDestroyNotify   destroy;   /* Destroy element callback. */
+  volatile gint    ref_count; /* Hidden Reference count. */
+} RgRingImpl;
+
+G_DEFINE_BOXED_TYPE (RgRing, rg_ring, rg_ring_ref, rg_ring_unref)
+
+/**
+ * rg_ring_sized_new:
+ * @element_size: (in): The size per element.
+ * @reserved_size: (in): The number of elements to allocate.
+ * @element_destroy: (in): Notification called when removing an element.
+ *
+ * Creates a new instance of #RgRing with the given number of elements.
+ *
+ * Returns: A new #RgRing.
+ */
+RgRing*
+rg_ring_sized_new (guint          element_size,
+                     guint          reserved_size,
+                     GDestroyNotify element_destroy)
+{
+  RgRingImpl *ring_impl;
+
+  ring_impl = g_slice_new0 (RgRingImpl);
+  ring_impl->elt_size = element_size;
+  ring_impl->len = reserved_size;
+  ring_impl->data = g_malloc0_n (reserved_size, element_size);
+  ring_impl->destroy = element_destroy;
+  ring_impl->ref_count = 1;
+
+  return (RgRing *)ring_impl;
+}
+
+/**
+ * rg_ring_append_vals:
+ * @ring: (in): A #RgRing.
+ * @data: (in): A pointer to the array of values.
+ * @len: (in): The number of values.
+ *
+ * Appends @len values located at @data.
+ *
+ * Returns: the index of the first item.
+ */
+guint
+rg_ring_append_vals (RgRing        *ring,
+                     gconstpointer  data,
+                     guint          len)
+{
+  RgRingImpl *ring_impl = (RgRingImpl *)ring;
+  gpointer idx;
+  gint ret = -1;
+  gint x;
+  gint i;
+
+  g_return_if_fail (ring_impl != NULL);
+  g_return_if_fail (len <= ring->len);
+  g_return_if_fail (len > 0);
+
+  for (i = 0; i < len; i++)
+    {
+      x = ring->pos - i;
+      x = (x >= 0) ? x : ring->len + x;
+      idx = ring->data + (ring_impl->elt_size * x);
+      if (ring_impl->destroy && (ring_impl->looped == TRUE))
+        ring_impl->destroy (idx);
+      if (ret == -1)
+        ret = x;
+      memcpy (idx, data, ring_impl->elt_size);
+      ring->pos++;
+      if (ring->pos >= ring->len)
+        ring_impl->looped = TRUE;
+      ring->pos %= ring->len;
+      data = ((guint8 *)data) + ring_impl->elt_size;
+    }
+
+  return (guint)ret;
+}
+
+/**
+ * rg_ring_foreach:
+ * @ring: (in): A #RgRing.
+ * @func: (in): A #GFunc to call for each element.
+ * @user_data: (in): user data for @func.
+ *
+ * Calls @func for every item in the #RgRing starting from the most recently
+ * inserted element to the least recently inserted.
+ */
+void
+rg_ring_foreach (RgRing *ring,
+                   GFunc     func,
+                   gpointer  user_data)
+{
+  RgRingImpl *ring_impl = (RgRingImpl *)ring;
+  gint i;
+
+  g_return_if_fail (ring_impl != NULL);
+  g_return_if_fail (func != NULL);
+
+  if (!ring_impl->looped)
+    {
+      for (i = 0; i < ring_impl->pos; i++)
+        func (get_element (ring_impl, i), user_data);
+      return;
+    }
+
+  for (i = ring_impl->pos; i < ring_impl->len; i++)
+    func (get_element (ring_impl, i), user_data);
+
+  for (i = 0; i < ring_impl->pos; i++)
+    func (get_element (ring_impl, i), user_data);
+}
+
+/**
+ * rg_ring_destroy:
+ * @ring: (in): A #RgRing.
+ *
+ * Cleans up after a #RgRing that is no longer in use.
+ */
+void
+rg_ring_destroy (RgRing *ring)
+{
+  RgRingImpl *ring_impl = (RgRingImpl *)ring;
+
+  g_return_if_fail (ring != NULL);
+  g_return_if_fail (ring_impl->ref_count == 0);
+
+  g_free (ring_impl->data);
+
+  g_slice_free (RgRingImpl, ring_impl);
+}
+
+/**
+ * rg_ring_ref:
+ * @ring: (in): A #RgRing.
+ *
+ * Atomically increments the reference count of @ring by one.
+ *
+ * Returns: The @ring pointer.
+ */
+RgRing *
+rg_ring_ref (RgRing *ring)
+{
+  RgRingImpl *ring_impl = (RgRingImpl *)ring;
+
+  g_return_val_if_fail (ring != NULL, NULL);
+  g_return_val_if_fail (ring_impl->ref_count > 0, NULL);
+
+  g_atomic_int_inc (&ring_impl->ref_count);
+
+  return ring;
+}
+
+/**
+ * rg_ring_unref:
+ * @ring: (in): A #RgRing.
+ *
+ * Atomically decrements the reference count of @ring by one.  When the
+ * reference count reaches zero, the structure is freed.
+ */
+void
+rg_ring_unref (RgRing *ring)
+{
+  RgRingImpl *ring_impl = (RgRingImpl *)ring;
+
+  g_return_if_fail (ring != NULL);
+  g_return_if_fail (ring_impl->ref_count > 0);
+
+  if (g_atomic_int_dec_and_test (&ring_impl->ref_count))
+    rg_ring_destroy (ring);
+}
diff --git a/contrib/rg/rg-ring.h b/contrib/rg/rg-ring.h
new file mode 100644
index 0000000..024fa0f
--- /dev/null
+++ b/contrib/rg/rg-ring.h
@@ -0,0 +1,80 @@
+/* rg-ring.h
+ *
+ * Copyright (C) 2010 Christian Hergert <chris dronelabs com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#ifndef __RG_RING_H__
+#define __RG_RING_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+/**
+ * rg_ring_append_val:
+ * @ring: A #RgRing.
+ * @val: A value to append to the #RgRing.
+ *
+ * Appends a value to the ring buffer.  @val must be a variable as it is
+ * referenced to.
+ *
+ * Returns: None.
+ */
+#define rg_ring_append_val(ring, val) rg_ring_append_vals(ring, &(val), 1)
+
+/**
+ * rg_ring_get_index:
+ * @ring: A #RgRing.
+ * @type: The type to extract.
+ * @i: The index within the #RgRing relative to the current position.
+ *
+ * Retrieves the value at the given index from the #RgRing.  The value
+ * is cast to @type.  You may retrieve a pointer to the value within the
+ * array by using &.
+ *
+ * [[
+ * gdouble *v = &rg_ring_get_index(ring, gdouble, 0);
+ * gdouble v = rg_ring_get_index(ring, gdouble, 0);
+ * ]]
+ *
+ * Returns: The value at the given index.
+ */
+#define rg_ring_get_index(ring, type, i) \
+  ((((type*)(ring)->data))[((i) + (ring)->pos) % (ring)->len])
+
+typedef struct
+{
+       guint8 *data;
+       guint   len;
+       guint   pos;
+} RgRing;
+
+GType   rg_ring_get_type    (void);
+RgRing *rg_ring_sized_new   (guint           element_size,
+                             guint           reserved_size,
+                             GDestroyNotify  element_destroy);
+guint   rg_ring_append_vals (RgRing         *ring,
+                             gconstpointer   data,
+                             guint           len);
+void    rg_ring_foreach     (RgRing         *ring,
+                             GFunc           func,
+                             gpointer        user_data);
+RgRing *rg_ring_ref         (RgRing         *ring);
+void    rg_ring_unref       (RgRing         *ring);
+
+G_END_DECLS
+
+#endif /* __RG_RING_H__ */
diff --git a/contrib/rg/rg-table.c b/contrib/rg/rg-table.c
new file mode 100644
index 0000000..b99314e
--- /dev/null
+++ b/contrib/rg/rg-table.c
@@ -0,0 +1,560 @@
+/* rg-table.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "rg-column-private.h"
+#include "rg-table.h"
+
+typedef struct
+{
+  GPtrArray *columns;
+  RgColumn  *timestamps;
+
+  guint      last_index;
+
+  guint      max_samples;
+  GTimeSpan  timespan;
+  gdouble    value_max;
+  gdouble    value_min;
+} RgTablePrivate;
+
+typedef struct
+{
+  RgTable *table;
+  gint64   timestamp;
+  guint    index;
+} RgTableIterImpl;
+
+enum {
+  PROP_0,
+  PROP_MAX_SAMPLES,
+  PROP_TIMESPAN,
+  PROP_VALUE_MAX,
+  PROP_VALUE_MIN,
+  LAST_PROP
+};
+
+enum {
+  CHANGED,
+  LAST_SIGNAL
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (RgTable, rg_table, G_TYPE_OBJECT)
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+static guint gSignals [LAST_SIGNAL];
+
+gint64
+rg_table_get_timespan (RgTable *self)
+{
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+
+  g_return_val_if_fail (RG_IS_TABLE (self), 0);
+
+  return priv->timespan;
+}
+
+void
+rg_table_set_timespan (RgTable   *self,
+                       GTimeSpan  timespan)
+{
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+
+  g_return_if_fail (RG_IS_TABLE (self));
+
+  if (timespan != priv->timespan)
+    {
+      priv->timespan = timespan;
+      g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_TIMESPAN]);
+    }
+}
+
+static void
+rg_table_set_value_max (RgTable *self,
+                        gdouble  value_max)
+{
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+
+  g_return_if_fail (RG_IS_TABLE (self));
+
+  if (priv->value_max != value_max)
+    {
+      priv->value_max = value_max;
+      g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_VALUE_MAX]);
+    }
+}
+
+static void
+rg_table_set_value_min (RgTable *self,
+                        gdouble  value_min)
+{
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+
+  g_return_if_fail (RG_IS_TABLE (self));
+
+  if (priv->value_min != value_min)
+    {
+      priv->value_min = value_min;
+      g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_VALUE_MIN]);
+    }
+}
+
+RgTable *
+rg_table_new (void)
+{
+  return g_object_new (RG_TYPE_TABLE, NULL);
+}
+
+guint
+rg_table_add_column (RgTable  *self,
+                     RgColumn *column)
+{
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+
+  g_return_if_fail (RG_IS_TABLE (self));
+  g_return_if_fail (RG_IS_COLUMN (column));
+
+  _rg_column_set_n_rows (column, priv->max_samples);
+
+  g_ptr_array_add (priv->columns, g_object_ref (column));
+
+  return priv->columns->len - 1;
+}
+
+guint
+rg_table_get_max_samples (RgTable *self)
+{
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+
+  g_return_val_if_fail (RG_IS_TABLE (self), 0);
+
+  return priv->max_samples;
+}
+
+void
+rg_table_set_max_samples (RgTable *self,
+                          guint    max_samples)
+{
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+  gsize i;
+
+  g_return_if_fail (RG_IS_TABLE (self));
+  g_return_if_fail (max_samples > 0);
+
+  if (max_samples == priv->max_samples)
+    return;
+
+  for (i = 0; i < priv->columns->len; i++)
+    {
+      RgColumn *column;
+
+      column = g_ptr_array_index (priv->columns, i);
+      _rg_column_set_n_rows (column, max_samples);
+    }
+
+  _rg_column_set_n_rows (priv->timestamps, max_samples);
+
+  priv->max_samples = max_samples;
+
+  g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_MAX_SAMPLES]);
+}
+
+void
+rg_table_push (RgTable     *self,
+               RgTableIter *iter,
+               gint64       timestamp)
+{
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+  RgTableIterImpl *impl = (RgTableIterImpl *)iter;
+  guint pos;
+  gsize i;
+
+  g_return_if_fail (RG_IS_TABLE (self));
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (timestamp > 0);
+
+  for (i = 0; i < priv->columns->len; i++)
+    {
+      RgColumn *column;
+
+      column = g_ptr_array_index (priv->columns, i);
+      _rg_column_push (column);
+    }
+
+  pos = _rg_column_push (priv->timestamps);
+  _rg_column_set (priv->timestamps, pos, timestamp);
+
+  impl->table = self;
+  impl->timestamp = timestamp;
+  impl->index = pos;
+
+  priv->last_index = pos;
+
+  g_signal_emit (self, gSignals [CHANGED], 0);
+}
+
+gboolean
+rg_table_get_iter_last (RgTable     *self,
+                        RgTableIter *iter)
+{
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+  RgTableIterImpl *impl = (RgTableIterImpl *)iter;
+
+  g_return_if_fail (RG_IS_TABLE (self));
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (impl != NULL);
+
+  impl->table = self;
+  impl->index = priv->last_index;
+  impl->timestamp = 0;
+
+  _rg_column_get (priv->timestamps, impl->index, &impl->timestamp);
+
+  return (impl->timestamp != 0);
+}
+
+gint64
+rg_table_get_end_time (RgTable *self)
+{
+  RgTableIter iter;
+
+  g_return_val_if_fail (RG_IS_TABLE (self), 0);
+
+  if (rg_table_get_iter_last (self, &iter))
+    return rg_table_iter_get_timestamp (&iter);
+
+  return g_get_monotonic_time ();
+}
+
+gboolean
+rg_table_get_iter_first (RgTable     *self,
+                         RgTableIter *iter)
+{
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+  RgTableIterImpl *impl = (RgTableIterImpl *)iter;
+
+  g_return_if_fail (RG_IS_TABLE (self));
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (impl != NULL);
+
+  impl->table = self;
+  impl->index = (priv->last_index + 1) % priv->max_samples;
+  impl->timestamp = 0;
+
+  _rg_column_get (priv->timestamps, impl->index, &impl->timestamp);
+
+  /*
+   * Maybe this is our first time around the ring, and we can just
+   * assume the 0 index is the real first entry.
+   */
+  if (impl->timestamp == 0)
+    {
+      impl->index = 0;
+      _rg_column_get (priv->timestamps, impl->index, &impl->timestamp);
+    }
+
+  return (impl->timestamp != 0);
+}
+
+gboolean
+rg_table_iter_next (RgTableIter *iter)
+{
+  RgTablePrivate *priv;
+  RgTableIterImpl *impl = (RgTableIterImpl *)iter;
+
+  g_return_val_if_fail (iter != NULL, FALSE);
+  g_return_val_if_fail (impl != NULL, FALSE);
+  g_return_val_if_fail (RG_IS_TABLE (impl->table), FALSE);
+
+  priv = rg_table_get_instance_private (impl->table);
+
+  if (impl->index == priv->last_index)
+    {
+      impl->table = NULL;
+      impl->index = 0;
+      impl->timestamp = 0;
+      return FALSE;
+    }
+
+  do
+    {
+      impl->index = (impl->index + 1) % priv->max_samples;
+
+      impl->timestamp = 0;
+      _rg_column_get (priv->timestamps, impl->index, &impl->timestamp);
+
+      if (impl->timestamp > 0)
+        break;
+    }
+  while (impl->index < priv->last_index);
+
+  return (impl->timestamp > 0);
+}
+
+gint64
+rg_table_iter_get_timestamp (RgTableIter *iter)
+{
+  RgTableIterImpl *impl = (RgTableIterImpl *)iter;
+
+  g_return_val_if_fail (iter != NULL, 0);
+
+  return impl->timestamp;
+}
+
+void
+rg_table_iter_set (RgTableIter *iter,
+                   gint         first_column,
+                   ...)
+{
+  RgTableIterImpl *impl = (RgTableIterImpl *)iter;
+  RgTablePrivate *priv;
+  gint column_id = first_column;
+  va_list args;
+
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (impl != NULL);
+  g_return_if_fail (RG_IS_TABLE (impl->table));
+
+  priv = rg_table_get_instance_private (impl->table);
+
+  va_start (args, first_column);
+
+  while (column_id >= 0)
+    {
+      RgColumn *column;
+
+      if (column_id >= priv->columns->len)
+        {
+          g_critical ("No such column %d", column_id);
+          return;
+        }
+
+      column = g_ptr_array_index (priv->columns, column_id);
+
+      _rg_column_collect (column, impl->index, args);
+
+      column_id = va_arg (args, gint);
+    }
+
+  if (column_id != -1)
+    g_critical ("Invalid column sentinal: %d", column_id);
+
+  va_end (args);
+}
+
+void
+rg_table_iter_get (RgTableIter *iter,
+                   gint         first_column,
+                   ...)
+{
+  RgTableIterImpl *impl = (RgTableIterImpl *)iter;
+  RgTablePrivate *priv;
+  gint column_id = first_column;
+  va_list args;
+
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (impl != NULL);
+  g_return_if_fail (RG_IS_TABLE (impl->table));
+
+  priv = rg_table_get_instance_private (impl->table);
+
+  va_start (args, first_column);
+
+  while (column_id >= 0)
+    {
+      RgColumn *column;
+
+      if (column_id >= priv->columns->len)
+        {
+          g_critical ("No such column %d", column_id);
+          return;
+        }
+
+      column = g_ptr_array_index (priv->columns, column_id);
+
+      _rg_column_lcopy (column, impl->index, args);
+
+      column_id = va_arg (args, gint);
+    }
+
+  if (column_id != -1)
+    g_critical ("Invalid column sentinal: %d", column_id);
+
+  va_end (args);
+}
+
+void
+rg_table_iter_get_value (RgTableIter *iter,
+                         guint        column,
+                         GValue      *value)
+{
+  RgTableIterImpl *impl = (RgTableIterImpl *)iter;
+  RgTablePrivate *priv;
+  RgColumn *col;
+
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (impl != NULL);
+  g_return_if_fail (RG_IS_TABLE (impl->table));
+  priv = rg_table_get_instance_private (impl->table);
+  g_return_if_fail (column < priv->columns->len);
+
+  col = g_ptr_array_index (priv->columns, column);
+  _rg_column_get_value (col, impl->index, value);
+}
+
+static void
+rg_table_finalize (GObject *object)
+{
+  RgTable *self = (RgTable *)object;
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+
+  g_clear_pointer (&priv->columns, g_object_unref);
+
+  G_OBJECT_CLASS (rg_table_parent_class)->finalize (object);
+}
+
+static void
+rg_table_get_property (GObject    *object,
+                       guint       prop_id,
+                       GValue     *value,
+                       GParamSpec *pspec)
+{
+  RgTable *self = (RgTable *)object;
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_TIMESPAN:
+      g_value_set_int64 (value, priv->timespan);
+      break;
+
+    case PROP_MAX_SAMPLES:
+      g_value_set_uint (value, priv->max_samples);
+      break;
+
+    case PROP_VALUE_MAX:
+      g_value_set_double (value, priv->value_max);
+      break;
+
+    case PROP_VALUE_MIN:
+      g_value_set_double (value, priv->value_min);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+rg_table_set_property (GObject      *object,
+                       guint         prop_id,
+                       const GValue *value,
+                       GParamSpec   *pspec)
+{
+  RgTable *self = (RgTable *)object;
+
+  switch (prop_id)
+    {
+    case PROP_MAX_SAMPLES:
+      rg_table_set_max_samples (self, g_value_get_uint (value));
+      break;
+
+    case PROP_TIMESPAN:
+      rg_table_set_timespan (self, g_value_get_int64 (value));
+      break;
+
+    case PROP_VALUE_MAX:
+      rg_table_set_value_max (self, g_value_get_double (value));
+      break;
+
+    case PROP_VALUE_MIN:
+      rg_table_set_value_min (self, g_value_get_double (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+rg_table_class_init (RgTableClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = rg_table_finalize;
+  object_class->get_property = rg_table_get_property;
+  object_class->set_property = rg_table_set_property;
+
+  gParamSpecs [PROP_MAX_SAMPLES] =
+    g_param_spec_uint ("max-samples",
+                       _("Max Samples"),
+                       _("Max Samples"),
+                       1, G_MAXUINT,
+                       120,
+                       (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_TIMESPAN] =
+    g_param_spec_int64 ("timespan",
+                        _("Timespan"),
+                        _("Timespan to visualize, in microseconds."),
+                        1, G_MAXINT64,
+                        G_USEC_PER_SEC * 60L,
+                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_VALUE_MAX] =
+    g_param_spec_double ("value-max",
+                         _("Value Max"),
+                         _("Value Max"),
+                         -G_MINDOUBLE, G_MAXDOUBLE,
+                         100.0,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_VALUE_MIN] =
+    g_param_spec_double ("value-min",
+                         _("Value Min"),
+                         _("Value Min"),
+                         -G_MINDOUBLE, G_MAXDOUBLE,
+                         100.0,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
+
+  gSignals [CHANGED] = g_signal_new ("changed",
+                                     G_TYPE_FROM_CLASS (klass),
+                                     G_SIGNAL_RUN_LAST,
+                                     0,
+                                     NULL, NULL, NULL,
+                                     G_TYPE_NONE,
+                                     0);
+}
+
+static void
+rg_table_init (RgTable *self)
+{
+  RgTablePrivate *priv = rg_table_get_instance_private (self);
+
+  priv->max_samples = 60;
+  priv->value_min = 0.0;
+  priv->value_max = 100.0;
+
+  priv->columns = g_ptr_array_new_with_free_func (g_object_unref);
+
+  priv->timestamps = rg_column_new (NULL, G_TYPE_INT64);
+  _rg_column_set_n_rows (priv->timestamps, priv->max_samples);
+}
diff --git a/contrib/rg/rg-table.h b/contrib/rg/rg-table.h
new file mode 100644
index 0000000..fb3ebaf
--- /dev/null
+++ b/contrib/rg/rg-table.h
@@ -0,0 +1,73 @@
+/* rg-table.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+#ifndef RG_TABLE_H
+#define RG_TABLE_H
+
+#include <glib-object.h>
+
+#include "rg-column.h"
+
+G_BEGIN_DECLS
+
+#define RG_TYPE_TABLE (rg_table_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (RgTable, rg_table, RG, TABLE, GObject)
+
+struct _RgTableClass
+{
+  GObjectClass parent;
+};
+
+typedef struct
+{
+  gpointer data[8];
+} RgTableIter;
+
+RgTable   *rg_table_new                (void);
+guint      rg_table_add_column         (RgTable     *self,
+                                        RgColumn    *column);
+GTimeSpan  rg_table_get_timespan       (RgTable     *self);
+void       rg_table_set_timespan       (RgTable     *self,
+                                        GTimeSpan    timespan);
+gint64     rg_table_get_end_time       (RgTable     *self);
+guint      rg_table_get_max_samples    (RgTable     *self);
+void       rg_table_set_max_samples    (RgTable     *self,
+                                        guint        n_rows);
+void       rg_table_push               (RgTable     *self,
+                                        RgTableIter *iter,
+                                        gint64       timestamp);
+gboolean   rg_table_get_iter_first     (RgTable     *self,
+                                        RgTableIter *iter);
+gboolean   rg_table_get_iter_last      (RgTable     *self,
+                                        RgTableIter *iter);
+gboolean   rg_table_iter_next          (RgTableIter *iter);
+void       rg_table_iter_get           (RgTableIter *iter,
+                                        gint         first_column,
+                                        ...);
+void       rg_table_iter_get_value     (RgTableIter *iter,
+                                        guint        column,
+                                        GValue      *value);
+gint64     rg_table_iter_get_timestamp (RgTableIter *iter);
+void       rg_table_iter_set           (RgTableIter *iter,
+                                        gint         first_column,
+                                        ...);
+
+G_END_DECLS
+
+#endif /* RG_TABLE_H */
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 1856aa4..fc749fa 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -11,6 +11,19 @@ egg_libs = \
        $(top_builddir)/contrib/egg/libegg.la \
        $(NULL)
 
+rg_cflags = \
+       $(DEBUG_CFLAGS) \
+       $(RG_CFLAGS) \
+       -I$(top_srcdir)/contrib/egg \
+       -I$(top_srcdir)/contrib/rg \
+       $(NULL)
+
+rg_libs = \
+       $(top_builddir)/contrib/egg/libegg.la \
+       $(top_builddir)/contrib/rg/librg.la \
+       $(RG_LIBS) \
+       $(NULL)
+
 tests_cflags = \
        $(egg_cflags) \
        -I$(top_srcdir)/libide \
@@ -22,6 +35,7 @@ tests_libs = \
        $(top_builddir)/libide/libide-1.0.la \
        $(NULL)
 
+misc_programs =
 
 TESTS_ENVIRONMENT= \
         GI_TYPELIB_PATH="$(top_srcdir)/libide"  \
@@ -104,12 +118,18 @@ test_vim_LDADD = \
        $(NULL)
 
 
-misc_programs = test-ide-source-view
+misc_programs += test-ide-source-view
 test_ide_source_view_SOURCES = test-ide-source-view.c
 test_ide_source_view_CFLAGS = $(tests_cflags)
 test_ide_source_view_LDADD = $(tests_libs)
 
 
+misc_programs += test-cpu-graph
+test_cpu_graph_SOURCES = test-cpu-graph.c
+test_cpu_graph_CFLAGS = $(rg_cflags)
+test_cpu_graph_LDADD = $(rg_libs)
+
+
 TESTS += test-ide-ctags
 test_ide_ctags_SOURCES = test-ide-ctags.c
 test_ide_ctags_CFLAGS = $(tests_cflags)
diff --git a/tests/test-cpu-graph.c b/tests/test-cpu-graph.c
new file mode 100644
index 0000000..3b8a7a3
--- /dev/null
+++ b/tests/test-cpu-graph.c
@@ -0,0 +1,102 @@
+#include "rg-graph.h"
+#include "rg-cpu-table.h"
+#include "rg-line-renderer.h"
+
+#include <stdlib.h>
+
+#define CSS_DATA \
+  "RgGraph {\n" \
+  "  background-color: #f6f7f8;\n" \
+  "  background-size: 8px 8px;\n" \
+  "  background-image:repeating-linear-gradient(0deg, #f0f1f2, #f0f1f2 1px, transparent 1px, transparent 
8px),repeating-linear-gradient(-90deg, #f0f1f2, #f0f1f2 1px, transparent 1px, transparent 8px);\n" \
+  "}"
+
+static gchar *colors[] = {
+  "#73d216",
+  "#f57900",
+  "#3465a4",
+  "#ef2929",
+  "#75507b",
+  "#ce5c00",
+  "#c17d11",
+  "#ce5c00",
+};
+
+int
+main (int argc,
+      char *argv[])
+{
+  guint samples = 2;
+  guint seconds = 30;
+  const GOptionEntry entries[] = {
+    { "samples", 'm', 0, G_OPTION_ARG_INT, &samples, "Number of samples per second", "2" },
+    { "seconds", 's', 0, G_OPTION_ARG_INT, &seconds, "Number of seconds to display", "60" },
+    { NULL }
+  };
+  gint64 timespan;
+  guint max_samples;
+  GOptionContext *context;
+  GtkWindow *window;
+  RgGraph *graph;
+  RgTable *table;
+  RgRenderer *renderer;
+  GtkCssProvider *provider;
+  GError *error = NULL;
+  gsize i;
+
+  context = g_option_context_new ("- a simple cpu graph");
+  g_option_context_add_group (context, gtk_get_option_group (TRUE));
+  g_option_context_add_main_entries (context, entries, NULL);
+
+  if (!g_option_context_parse (context, &argc, &argv, &error))
+    {
+      g_printerr ("%s\n", error->message);
+      return EXIT_FAILURE;
+    }
+
+  g_print ("%d samples per second over %d seconds.\n",
+           samples, seconds);
+
+  timespan = (gint64)seconds * G_USEC_PER_SEC;
+  max_samples = seconds * samples;
+
+  table = g_object_new (RG_TYPE_CPU_TABLE,
+                        "timespan", timespan,
+                        "max-samples", max_samples,
+                        NULL);
+
+  provider = gtk_css_provider_new ();
+  gtk_css_provider_load_from_data (provider, CSS_DATA, -1, NULL);
+  gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), GTK_STYLE_PROVIDER (provider), 
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+  g_object_unref (provider);
+
+  window = g_object_new (GTK_TYPE_WINDOW,
+                         "default-width", 600,
+                         "default-height", 325,
+                         "title", "CPU Graph",
+                         NULL);
+
+  graph = g_object_new (RG_TYPE_GRAPH,
+                        "visible", TRUE,
+                        "table", table,
+                        NULL);
+
+  for (i = 0; i < g_get_num_processors (); i++)
+    {
+      renderer = g_object_new (RG_TYPE_LINE_RENDERER,
+                               "column", i,
+                               "stroke-color", colors[i % G_N_ELEMENTS (colors)],
+                               "line-width", 1.0,
+                               NULL);
+      rg_graph_add_renderer (graph, renderer);
+      g_object_unref (renderer);
+    }
+
+  gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (graph));
+
+  g_signal_connect (window, "delete-event", gtk_main_quit, NULL);
+  gtk_window_present (window);
+  gtk_main ();
+
+  return 0;
+}


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