[gjs/wip/chergert/profiler] profiler: add SPSProfiler implementation



commit 149e9a8279d5a9a3d6c6b9444308d8f7e7d7ec9b
Author: Christian Hergert <christian hergert me>
Date:   Mon Jul 31 15:39:10 2017 +0100

    profiler: add SPSProfiler implementation
    
    Currently this is Linux only as it requires the Linux implementation of
    POSIX signals.
    
    There is a simple API, and the profile output is written to
    /tmp/gjs-profile-<pid>. Description of the capture format is provided
    in profile.cpp.
    
    For apps started with gjs-console, you can toggle on/off profiling by
    sending SIGUSR2 to the process.

 gjs-srcs.mk              |    7 +
 gjs/console.cpp          |   14 +
 gjs/gjs.h                |    1 +
 gjs/profiler.cpp         |  552 +++++++++++++++++++++++++++
 gjs/profiler.h           |   44 +++
 test/gjs-tests.cpp       |   32 ++
 util/sp-capture-reader.c |  686 +++++++++++++++++++++++++++++++++
 util/sp-capture-reader.h |   65 ++++
 util/sp-capture-types.h  |  155 ++++++++
 util/sp-capture-writer.c |  954 ++++++++++++++++++++++++++++++++++++++++++++++
 util/sp-capture-writer.h |  108 ++++++
 11 files changed, 2618 insertions(+), 0 deletions(-)
---
diff --git a/gjs-srcs.mk b/gjs-srcs.mk
index 5d41951..25fa7c2 100644
--- a/gjs-srcs.mk
+++ b/gjs-srcs.mk
@@ -79,6 +79,8 @@ gjs_srcs =                            \
        gjs/module.cpp                  \
        gjs/native.cpp                  \
        gjs/native.h                    \
+       gjs/profiler.cpp                \
+       gjs/profiler.h                  \
        gjs/stack.cpp                   \
        modules/modules.cpp             \
        modules/modules.h               \
@@ -91,6 +93,11 @@ gjs_srcs =                           \
        util/log.h                      \
        util/misc.cpp                   \
        util/misc.h                     \
+       util/sp-capture-reader.c        \
+       util/sp-capture-reader.h        \
+       util/sp-capture-types.h         \
+       util/sp-capture-writer.c        \
+       util/sp-capture-writer.h        \
        $(NULL)
 
 # These files were part of a separate library
diff --git a/gjs/console.cpp b/gjs/console.cpp
index fa6c579..0192f88 100644
--- a/gjs/console.cpp
+++ b/gjs/console.cpp
@@ -33,6 +33,7 @@
 static char **include_path = NULL;
 static char **coverage_prefixes = NULL;
 static char *coverage_output_path = NULL;
+static char *profile_output_path = nullptr;
 static char *command = NULL;
 static gboolean print_version = false;
 
@@ -42,6 +43,7 @@ static GOptionEntry entries[] = {
     { "coverage-prefix", 'C', 0, G_OPTION_ARG_STRING_ARRAY, &coverage_prefixes, "Add the prefix PREFIX to 
the list of files to generate coverage info for", "PREFIX" },
     { "coverage-output", 0, 0, G_OPTION_ARG_STRING, &coverage_output_path, "Write coverage output to a 
directory DIR. This option is mandatory when using --coverage-path", "DIR", },
     { "include-path", 'I', 0, G_OPTION_ARG_STRING_ARRAY, &include_path, "Add the directory DIR to the list 
of directories to search for js files.", "DIR" },
+    { "profile-output", 0, 0, G_OPTION_ARG_FILENAME, &profile_output_path, "Enable the profiler and Write 
output to FILE", "FILE" },
     { NULL }
 };
 
@@ -154,6 +156,7 @@ main(int argc, char **argv)
     GError *error = NULL;
     GjsContext *js_context;
     GjsCoverage *coverage = NULL;
+    GjsProfiler *profiler;
     char *script;
     const char *filename;
     const char *program_name;
@@ -267,6 +270,16 @@ main(int argc, char **argv)
         g_object_unref(output);
     }
 
+    profiler = gjs_profiler_new(js_context);
+
+    /* Allow SIGUSR2 (with sigaction param) to enable/disable */
+    gjs_profiler_setup_signals();
+
+    if (profile_output_path) {
+        gjs_profiler_set_filename(profiler, profile_output_path);
+        gjs_profiler_start(profiler);
+    }
+
     /* prepare command line arguments */
     if (!gjs_context_define_string_array(js_context, "ARGV",
                                          script_argc, (const char **) script_argv,
@@ -297,6 +310,7 @@ main(int argc, char **argv)
     g_strfreev(coverage_prefixes);
     if (coverage)
         g_object_unref(coverage);
+    gjs_profiler_free(profiler);
     g_object_unref(js_context);
     g_free(script);
     exit(code);
diff --git a/gjs/gjs.h b/gjs/gjs.h
index dbee71c..b044d0f 100644
--- a/gjs/gjs.h
+++ b/gjs/gjs.h
@@ -27,6 +27,7 @@
 #include <gjs/macros.h>
 #include <gjs/context.h>
 #include <gjs/coverage.h>
+#include <gjs/profiler.h>
 #include <util/error.h>
 
 #endif /* __GJS_GJS_H__ */
diff --git a/gjs/profiler.cpp b/gjs/profiler.cpp
new file mode 100644
index 0000000..bff5dad
--- /dev/null
+++ b/gjs/profiler.cpp
@@ -0,0 +1,552 @@
+/* profiler.cpp
+ *
+ * Copyright (C) 2016 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 2 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 <config.h>
+
+#include <alloca.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <glib-unix.h>
+#include <signal.h>
+#include <sys/syscall.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+#include "jsapi-wrapper.h"
+#include <js/ProfilingStack.h>
+
+#include "profiler.h"
+#include "util/sp-capture-writer.h"
+
+/*
+ * This is mostly non-exciting code wrapping the builtin Profiler in
+ * mozjs. In particular, the profiler consumer is required to "bring your
+ * own sampler".  We do the very non-surprising thing of using POSIX
+ * timers to deliver SIGPROF to the thread containing the JSContext.
+ *
+ * However, we do use a Linux'ism that allows us to deliver the signal
+ * to only a single thread. Doing this in a generic fashion would
+ * require thread-registration so that we can mask SIGPROF from all
+ * threads execpt the JS thread. The gecko engine uses tgkill() to do
+ * this with a secondary thread instead of using POSIX timers. We could
+ * do this too, but it would still be Linux-only.
+ *
+ * Another option might be to use pthread_kill() and a secondary thread
+ * to perform the notification.
+ *
+ * From within the signal handler, we process the current stack as
+ * delivered to us from the JSRuntime. Any pointer data that comes from
+ * the runtime has to be copied, so we keep our own dedup'd string
+ * pointers for JavaScript file/line information. Non-JS instruction
+ * pointers are just fine, as they can be resolved by parsing the ELF for
+ * the file mapped on disk containing that address.
+ *
+ * As much of this code has to run from signal handlers, it is very
+ * important that we don't use anything that can malloc() or lock, or
+ * deadlocks are very likely. Most of GjsProfilerCapture is signal-safe.
+ */
+
+#define SAMPLES_PER_SEC     G_GUINT64_CONSTANT(1000)
+#define NSEC_PER_SEC        G_GUINT64_CONSTANT(1000000000)
+
+G_DEFINE_POINTER_TYPE (GjsProfiler, gjs_profiler)
+
+struct _GjsProfiler
+{
+
+  /* The stack for the JSRuntime profiler to use for current stack
+   * information while executing. We will look into this during our
+   * SIGPROF handler.
+   */
+  js::ProfileEntry stack[1024];
+
+  /* The context being profiled */
+  JSContext *context;
+
+  /* Buffers and writes our sampled stacks */
+  SpCaptureWriter *capture;
+
+  /* The filename to write to */
+  gchar *filename;
+
+  /* Our POSIX timer to wakeup SIGPROF */
+  timer_t timer;
+
+  /* The depth of @stack. This value may be larger than the
+   * number of elements in stack, and so you MUST ensure you
+   * don't walk past the end of stack[] when iterating.
+   */
+  uint32_t stack_depth;
+
+  /* Cached copy of our pid */
+  GPid pid;
+
+  /* If we are currently sampling */
+  guint running : 1;
+
+  /* If we should shutdown */
+  guint shutdown : 1;
+};
+
+static GjsProfiler *current_profiler;
+
+/*
+ * sample_capture_write_maps:
+ *
+ * This function will write the mapped section information to the
+ * capture file so that the callgraph builder can generate symbols
+ * from the stack addresses provided.
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and the profile
+ *   should abort.
+ */
+static gboolean
+gjs_profiler_extract_maps (GjsProfiler *self)
+{
+  g_auto(GStrv) lines = NULL;
+  g_autofree gchar *path = NULL;
+  g_autofree gchar *content = NULL;
+  gint64 now = g_get_monotonic_time () * 1000L;
+  gsize len;
+  guint i;
+
+  g_assert (self != NULL);
+
+  path = g_strdup_printf ("/proc/%u/maps", (guint)getpid ());
+  if (!g_file_get_contents (path, &content, &len, NULL))
+    return FALSE;
+
+  lines = g_strsplit (content, "\n", 0);
+
+  for (i = 0; lines [i] != NULL; i++)
+    {
+      gchar file[256];
+      gulong start;
+      gulong end;
+      gulong offset;
+      gulong inode;
+      gint r;
+
+      file [sizeof file - 1] = '\0';
+
+      r = sscanf (lines [i],
+                  "%lx-%lx %*15s %lx %*x:%*x %lu %255s",
+                  &start, &end, &offset, &inode, file);
+
+      if (r != 5)
+        continue;
+
+      if (strcmp ("[vdso]", file) == 0)
+        {
+          offset = 0;
+          inode = 0;
+        }
+
+      if (!sp_capture_writer_add_map (self->capture, now, -1, self->pid, start, end, offset, inode, file))
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+/**
+ * gjs_profiler_new:
+ * @context: The #GjsContext to profile
+ *
+ * This creates a new profiler for the #JSRuntime. It is important that
+ * this structure is freed with gjs_profiler_free() before runtime is
+ * destroyed.
+ *
+ * Call gjs_profiler_start() to enable the profiler, and gjs_profiler_stop()
+ * when you have finished.
+ *
+ * The profiler works by enabling the JS profiler in spidermonkey so that
+ * sample information is available. A POSIX timer is used to signal SIGPROF
+ * to the process on a regular interval to collect the most recent profile
+ * sample and stash it away. It is a programming error to mask SIGPROF from
+ * the thread controlling the JS context.
+ *
+ * Returns: (transfer full): A newly allocated #GjsProfiler
+ */
+GjsProfiler *
+gjs_profiler_new (GjsContext *context)
+{
+  JSContext *js_context;
+  GjsProfiler *self = NULL;
+
+  g_return_val_if_fail (context != NULL, NULL);
+
+  js_context = (JSContext *)gjs_context_get_native_context (context);
+
+  self = g_new0 (GjsProfiler, 1);
+  self->context = js_context;
+
+  self->pid = getpid ();
+
+  current_profiler = self;
+
+  return self;
+}
+
+/**
+ * gjs_profiler_free:
+ * @self: A #GjsProfile
+ *
+ * Frees a profiler instance and cleans up any allocated data.
+ *
+ * If the profiler is running, it will be stopped. This may result in blocking
+ * to write the contents of the buffer to the underlying file-descriptor.
+ */
+void
+gjs_profiler_free (GjsProfiler *self)
+{
+  if (self != NULL)
+    {
+      if (self->running)
+        gjs_profiler_stop (self);
+      g_clear_pointer (&self->capture, sp_capture_writer_unref);
+      g_free (self);
+    }
+}
+
+/**
+ * gjs_profile_is_running:
+ *
+ * Checks if the profiler is currently running. This means that the JS
+ * profiler is enabled and POSIX signal timers are registered.
+ *
+ * Returns: %TRUE if the profiler is active.
+ */
+gboolean
+gjs_profiler_is_running (GjsProfiler *self)
+{
+  g_return_val_if_fail (self != NULL, FALSE);
+
+  return self->running;
+}
+
+static inline guint
+gjs_profiler_get_stack_size (GjsProfiler *self)
+{
+  g_assert (self != NULL);
+
+  /*
+   * Note that stack_depth could be larger than the number of
+   * items we have in our stack space. We must protect ourselves
+   * against overflowing by discarding anything after that depth
+   * of the stack.
+   */
+  return MIN (self->stack_depth, G_N_ELEMENTS (self->stack));
+}
+
+static void
+gjs_profiler_sigprof (int        signum,
+                      siginfo_t *info,
+                      void      *context)
+{
+  GjsProfiler *self = current_profiler;
+  SpCaptureAddress *addrs;
+  gint64 now;
+  guint depth;
+  guint i;
+
+  g_assert (info != NULL);
+  g_assert (info->si_signo == SIGPROF);
+
+  /*
+   * NOTE:
+   *
+   * This is the SIGPROF signal handler. Everything done in this thread
+   * needs to be things that are safe to do in a signal handler. One thing
+   * that is not okay to do, is *malloc*.
+   */
+
+  if ((self == NULL) || (info->si_code != SI_TIMER))
+    return;
+
+  if (0 == (depth = gjs_profiler_get_stack_size (self)))
+    return;
+
+  G_STATIC_ASSERT (G_N_ELEMENTS (self->stack) < G_MAXUSHORT);
+
+  now = g_get_monotonic_time () * 1000L;
+  addrs = (SpCaptureAddress *)alloca (sizeof *addrs * depth);
+
+  for (i = 0; i < depth; i++)
+    {
+      js::ProfileEntry *entry = &self->stack[i];
+      const gchar *label = entry->label();
+      guint flipped = depth - 1 - i;
+
+      /*
+       * SPSProfiler will put "js::RunScript" on the stack, but it has
+       * a stack address of "this", which is not terribly useful since
+       * everything will show up as [stack] when building callgraphs.
+       */
+      if (label != NULL)
+        addrs[flipped] = sp_capture_writer_add_jitmap (self->capture, label);
+      else
+        addrs[flipped] = (SpCaptureAddress)entry->stackAddress ();
+    }
+
+  if (!sp_capture_writer_add_sample (self->capture, now, -1, self->pid, addrs, depth))
+    gjs_profiler_stop (self);
+}
+
+/**
+ * gjs_profiler_start:
+ * @self: A #GjsProfiler
+ *
+ * As expected, this starts the GjsProfiler.
+ *
+ * This will enable the underlying JS profiler and register a POSIX timer to
+ * deliver SIGPROF on the configured sampling frequency.
+ *
+ * To reduce sampling overhead, #GjsProfiler stashes information about the
+ * profile to be calculated once the profiler has been disabled. Calling
+ * gjs_profile_stop() will result in that delayed work to be completed.
+ *
+ * You should call gjs_profiler_stop() when the profiler is no longer needed.
+ */
+void
+gjs_profiler_start (GjsProfiler *self)
+{
+  g_autofree gchar *filename = NULL;
+  g_autofree gchar *path = NULL;
+  struct sigaction sa = { 0 };
+  struct sigevent sev = { 0 };
+  struct itimerspec its = { 0 };
+  struct itimerspec old_its;
+
+  g_assert (self != NULL);
+  g_assert (self->capture == NULL);
+
+  if (self->running)
+    return;
+
+  path = g_strdup (self->filename);
+
+  if (path == NULL)
+    {
+      filename = g_strdup_printf ("gjs-profile-%u", getpid ());
+      path = g_build_filename (g_get_tmp_dir (), filename, NULL);
+    }
+
+  self->capture = sp_capture_writer_new (path, 0);
+
+  if (self->capture == NULL)
+    {
+      g_warning ("Failed to open profile capture");
+      return;
+    }
+
+  if (!gjs_profiler_extract_maps (self))
+    {
+      g_warning ("Failed to extract proc maps");
+      return;
+    }
+
+  self->stack_depth = 0;
+
+  /* Setup our signal handler for SIGPROF delivery */
+  sa.sa_flags = SA_RESTART | SA_SIGINFO;
+  sa.sa_sigaction = gjs_profiler_sigprof;
+  sigemptyset (&sa.sa_mask);
+
+  if (sigaction (SIGPROF, &sa, NULL) == -1)
+    {
+      g_warning ("Failed to register sigaction handler: %s",
+                 g_strerror (errno));
+      return;
+    }
+
+  /*
+   * Create our SIGPROF timer
+   *
+   * We want to receive a SIGPROF signal on the JS thread using our
+   * configured sampling frequency. Instead of allowing any thread to be
+   * notified, we set the _tid value to ensure that only our thread gets
+   * delivery of the signal. This feature is generally just for
+   * threading implementations, but it works for us as well and ensures
+   * that the thread is blocked while we capture the stack.
+   */
+  sev.sigev_notify = SIGEV_THREAD_ID;
+  sev.sigev_signo = SIGPROF;
+  sev._sigev_un._tid = syscall (__NR_gettid);
+
+  if (timer_create (CLOCK_MONOTONIC, &sev, &self->timer) == -1)
+    {
+      g_warning ("Failed to create profiler timer: %s", g_strerror (errno));
+      g_clear_pointer (&self->capture, sp_capture_writer_unref);
+      return;
+    }
+
+  /* Calculate sampling interval */
+  its.it_interval.tv_sec = 0;
+  its.it_interval.tv_nsec = NSEC_PER_SEC / SAMPLES_PER_SEC;
+  its.it_value.tv_sec = 0;
+  its.it_value.tv_nsec = NSEC_PER_SEC / SAMPLES_PER_SEC;
+
+  /* Now start this timer */
+  if (0 != timer_settime (self->timer, 0, &its, &old_its))
+    {
+      g_warning ("Failed to enable profiler timer: %s", g_strerror (errno));
+      timer_delete (self->timer);
+      g_clear_pointer (&self->capture, sp_capture_writer_unref);
+      return;
+    }
+
+  self->running = TRUE;
+
+  /* Notify the JS runtime of where to put stack info */
+  js::SetContextProfilingStack (self->context,
+                                self->stack,
+                                &self->stack_depth,
+                                G_N_ELEMENTS (self->stack));
+
+  /* Start recording stack info */
+  js::EnableContextProfilingStack (self->context, TRUE);
+
+  g_message ("Profiler started");
+}
+
+/**
+ * gjs_profiler_stop:
+ * @self: A #GjsProfiler
+ *
+ * Stops a currently running #GjsProfiler. If the profiler is not running,
+ * this function will do nothing.
+ *
+ * Some work may be delayed until the end of the capture. Such delayed work
+ * includes flushing the resulting samples and file location information to
+ * disk.
+ *
+ * This may block while writing to disk. Generally, the writes are delivered
+ * to a tmpfs device, and are therefore negligible.
+ */
+void
+gjs_profiler_stop (GjsProfiler *self)
+{
+  struct itimerspec its = { 0 };
+
+  g_assert (self != NULL);
+
+  if (!self->running)
+    return;
+
+  if (self == current_profiler)
+    current_profiler = NULL;
+
+  timer_settime (self->timer, 0, &its, NULL);
+  timer_delete (self->timer);
+
+  js::EnableContextProfilingStack (self->context, FALSE);
+
+  sp_capture_writer_flush (self->capture);
+
+  g_clear_pointer (&self->capture, sp_capture_writer_unref);
+
+  self->stack_depth = 0;
+  self->running = FALSE;
+
+  g_message ("Profiler stopped");
+}
+
+static gboolean
+gjs_profiler_sigusr2 (gpointer user_data)
+{
+  if (current_profiler != NULL)
+    {
+      if (gjs_profiler_is_running (current_profiler))
+        gjs_profiler_stop (current_profiler);
+      else
+        gjs_profiler_start (current_profiler);
+    }
+
+  return G_SOURCE_CONTINUE;
+}
+
+/**
+ * gjs_profiler_setup_signals:
+ *
+ * If you want to simply allow profiling of your process with minimal
+ * fuss, simply call gjs_profiler_setup_signals(). This will allow
+ * enabling and disabling the profiler with SIGUSR2. You must call
+ * this from main() immediately when your program starts and must not
+ * block SIGUSR2 from your signal mask.
+ *
+ * If this is not sufficient, use gjs_profiler_chain_signal() from your
+ * own signal handler to pass the signal to a GjsProfiler.
+ */
+void
+gjs_profiler_setup_signals (void)
+{
+  static gboolean initialized = FALSE;
+
+  if (!initialized)
+    {
+      initialized = TRUE;
+      g_unix_signal_add (SIGUSR2, gjs_profiler_sigusr2, NULL);
+    }
+}
+
+/**
+ * gjs_profiler_chain_signal:
+ *
+ * Use this to pass a signal handler caught by another signal handler
+ * to a GjsProfiler. This might be needed if you have your own complex
+ * signal handling system for which GjsProfiler cannot simply add a
+ * SIGUSR2 handler.
+ *
+ * This function should only be called from the JS thread.
+ *
+ * Returns: %TRUE if the signal was handled.
+ */
+gboolean
+gjs_profiler_chain_signal (siginfo_t *info)
+{
+  if (info != NULL)
+    {
+      if (info->si_signo == SIGPROF)
+        {
+          gjs_profiler_sigprof (SIGPROF, info, NULL);
+          return TRUE;
+        }
+
+      if (info->si_signo == SIGUSR2)
+        {
+          gjs_profiler_sigusr2 (NULL);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+void
+gjs_profiler_set_filename (GjsProfiler *self,
+                           const gchar *filename)
+{
+  g_return_if_fail (self != NULL);
+  g_return_if_fail (self->running == FALSE);
+
+  g_free (self->filename);
+  self->filename = g_strdup (filename);
+}
diff --git a/gjs/profiler.h b/gjs/profiler.h
new file mode 100644
index 0000000..4b11914
--- /dev/null
+++ b/gjs/profiler.h
@@ -0,0 +1,44 @@
+/* profiler.h
+ *
+ * Copyright (C) 2016 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 2 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 GJS_PROFILER_H
+#define GJS_PROFILER_H
+
+#include <gjs/context.h>
+#include <signal.h>
+
+G_BEGIN_DECLS
+
+#define GJS_TYPE_PROFILER (gjs_profiler_get_type())
+
+typedef struct _GjsProfiler GjsProfiler;
+
+GType        gjs_profiler_get_type      (void);
+GjsProfiler *gjs_profiler_new           (GjsContext  *context);
+void         gjs_profiler_free          (GjsProfiler *self);
+void         gjs_profiler_set_filename  (GjsProfiler *self,
+                                         const gchar *filename);
+void         gjs_profiler_start         (GjsProfiler *self);
+void         gjs_profiler_stop          (GjsProfiler *self);
+gboolean     gjs_profiler_is_running    (GjsProfiler *self);
+void         gjs_profiler_setup_signals (void);
+gboolean     gjs_profiler_chain_signal  (siginfo_t   *info);
+
+G_END_DECLS
+
+#endif /* GJS_PROFILER_H */
diff --git a/test/gjs-tests.cpp b/test/gjs-tests.cpp
index fefc2ef..2ab203d 100644
--- a/test/gjs-tests.cpp
+++ b/test/gjs-tests.cpp
@@ -375,6 +375,37 @@ gjstest_test_strip_shebang_return_null_for_just_shebang(void)
     g_assert(line_number == -1);
 }
 
+static void
+gjstest_test_profiler_start_stop(void)
+{
+    GjsContext *context;
+    GjsProfiler *profiler;
+    guint i;
+
+    context = gjs_context_new ();
+    profiler = gjs_profiler_new (context);
+
+    gjs_profiler_start(profiler);
+
+    for (i = 0; i < 100000; i++)
+      {
+        GError *error = NULL;
+        int estatus;
+
+#define TESTJS "[1,5,7,1,2,3,67,8].sort()"
+
+        if (!gjs_context_eval (context, TESTJS, -1, "<input>", &estatus, &error))
+          g_printerr ("ERROR: %s", error->message);
+
+#undef TESTJS
+      }
+
+    gjs_profiler_stop(profiler);
+    gjs_profiler_free(profiler);
+
+    g_object_unref (context);
+}
+
 int
 main(int    argc,
      char **argv)
@@ -393,6 +424,7 @@ main(int    argc,
     g_test_add_func("/gjs/jsutil/strip_shebang/no_shebang", 
gjstest_test_strip_shebang_no_advance_for_no_shebang);
     g_test_add_func("/gjs/jsutil/strip_shebang/have_shebang", 
gjstest_test_strip_shebang_advance_for_shebang);
     g_test_add_func("/gjs/jsutil/strip_shebang/only_shebang", 
gjstest_test_strip_shebang_return_null_for_just_shebang);
+    g_test_add_func("/gjs/profiler/start_stop", gjstest_test_profiler_start_stop);
     g_test_add_func("/util/glib/strv/concat/null", gjstest_test_func_util_glib_strv_concat_null);
     g_test_add_func("/util/glib/strv/concat/pointers", gjstest_test_func_util_glib_strv_concat_pointers);
 
diff --git a/util/sp-capture-reader.c b/util/sp-capture-reader.c
new file mode 100644
index 0000000..f5d4544
--- /dev/null
+++ b/util/sp-capture-reader.c
@@ -0,0 +1,686 @@
+/* sp-capture-reader.c
+ *
+ * Copyright (C) 2016 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 2 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 <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <sys/sendfile.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "sp-capture-reader.h"
+#include "sp-capture-writer.h"
+
+struct _SpCaptureReader
+{
+  volatile gint        ref_count;
+  gchar               *filename;
+  guint8              *buf;
+  gsize                bufsz;
+  gsize                len;
+  gsize                pos;
+  gsize                fd_off;
+  int                  fd;
+  gint                 endian;
+  SpCaptureFileHeader  header;
+};
+
+#ifndef SP_DISABLE_GOBJECT
+G_DEFINE_BOXED_TYPE (SpCaptureReader, sp_capture_reader,
+                     sp_capture_reader_ref, sp_capture_reader_unref)
+#endif
+
+static gboolean
+sp_capture_reader_read_file_header (SpCaptureReader      *self,
+                                    SpCaptureFileHeader  *header,
+                                    GError              **error)
+{
+  g_assert (self != NULL);
+  g_assert (header != NULL);
+
+  if (sizeof *header != pread (self->fd, header, sizeof *header, 0L))
+    {
+      g_set_error (error,
+                   G_FILE_ERROR,
+                   g_file_error_from_errno (errno),
+                   "%s", g_strerror (errno));
+      return FALSE;
+    }
+
+  if (header->magic != SP_CAPTURE_MAGIC)
+    {
+      g_set_error (error,
+                   G_FILE_ERROR,
+                   G_FILE_ERROR_FAILED,
+                   "Capture file magic does not match");
+      return FALSE;
+    }
+
+  header->capture_time[sizeof header->capture_time - 1] = '\0';
+
+  return TRUE;
+}
+
+static void
+sp_capture_reader_finalize (SpCaptureReader *self)
+{
+  if (self != NULL)
+    {
+      close (self->fd);
+      g_free (self->buf);
+      g_free (self->filename);
+      g_free (self);
+    }
+}
+
+const gchar *
+sp_capture_reader_get_time (SpCaptureReader *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+
+  return self->header.capture_time;
+}
+
+const gchar *
+sp_capture_reader_get_filename (SpCaptureReader *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+
+  return self->filename;
+}
+
+SpCaptureReader *
+sp_capture_reader_new_from_fd (int      fd,
+                               GError **error)
+{
+  SpCaptureReader *self;
+
+  g_assert (fd > -1);
+
+  self = g_new0 (SpCaptureReader, 1);
+  self->ref_count = 1;
+  self->bufsz = G_MAXUSHORT * 2;
+  self->buf = g_malloc (self->bufsz);
+  self->len = 0;
+  self->pos = 0;
+  self->fd = fd;
+  self->fd_off = sizeof (SpCaptureFileHeader);
+
+  if (!sp_capture_reader_read_file_header (self, &self->header, error))
+    {
+      sp_capture_reader_finalize (self);
+      return NULL;
+    }
+
+  if (self->header.little_endian)
+    self->endian = G_LITTLE_ENDIAN;
+  else
+    self->endian = G_BIG_ENDIAN;
+
+  return self;
+}
+
+SpCaptureReader *
+sp_capture_reader_new (const gchar  *filename,
+                       GError      **error)
+{
+  SpCaptureReader *self;
+  int fd;
+
+  g_assert (filename != NULL);
+
+  if (-1 == (fd = open (filename, O_RDONLY, 0)))
+    {
+      g_set_error (error,
+                   G_FILE_ERROR,
+                   g_file_error_from_errno (errno),
+                   "%s", g_strerror (errno));
+      return NULL;
+    }
+
+  if (NULL == (self = sp_capture_reader_new_from_fd (fd, error)))
+    {
+      close (fd);
+      return NULL;
+    }
+
+  self->filename = g_strdup (filename);
+
+  return self;
+}
+
+static inline void
+sp_capture_reader_bswap_frame (SpCaptureReader *self,
+                               SpCaptureFrame  *frame)
+{
+  g_assert (self != NULL);
+  g_assert (frame!= NULL);
+
+  if (G_UNLIKELY (self->endian != G_BYTE_ORDER))
+    {
+      frame->len = GUINT16_SWAP_LE_BE (frame->len);
+      frame->cpu = GUINT16_SWAP_LE_BE (frame->len);
+      frame->pid = GUINT32_SWAP_LE_BE (frame->len);
+      frame->time = GUINT64_SWAP_LE_BE (frame->len);
+    }
+}
+
+static inline void
+sp_capture_reader_bswap_map (SpCaptureReader *self,
+                             SpCaptureMap    *map)
+{
+  g_assert (self != NULL);
+  g_assert (map != NULL);
+
+  if (G_UNLIKELY (self->endian != G_BYTE_ORDER))
+    {
+      map->start = GUINT64_SWAP_LE_BE (map->start);
+      map->end = GUINT64_SWAP_LE_BE (map->end);
+      map->offset = GUINT64_SWAP_LE_BE (map->offset);
+      map->inode = GUINT64_SWAP_LE_BE (map->inode);
+    }
+}
+
+static inline void
+sp_capture_reader_bswap_jitmap (SpCaptureReader *self,
+                                SpCaptureJitmap *jitmap)
+{
+  g_assert (self != NULL);
+  g_assert (jitmap != NULL);
+
+  if (G_UNLIKELY (self->endian != G_BYTE_ORDER))
+    jitmap->n_jitmaps = GUINT64_SWAP_LE_BE (jitmap->n_jitmaps);
+}
+
+static gboolean
+sp_capture_reader_ensure_space_for (SpCaptureReader *self,
+                                    gsize            len)
+{
+  g_assert (self != NULL);
+  g_assert (len > 0);
+
+  if ((self->len - self->pos) < len)
+    {
+      gssize r;
+
+      g_assert (self->len >= self->pos);
+
+      memmove (self->buf, &self->buf[self->pos], self->len - self->pos);
+      self->len -= self->pos;
+      self->pos = 0;
+
+      while ((self->len - self->pos) <= len)
+        {
+          g_assert (self->pos + self->len < self->bufsz);
+
+          /* Read into our buffer after our current read position */
+          r = pread (self->fd,
+                     &self->buf[self->len],
+                     self->bufsz - self->len,
+                     self->fd_off);
+
+          if (r <= 0)
+            break;
+
+          self->fd_off += r;
+          self->len += r;
+        }
+    }
+
+  return (self->len - self->pos) >= len;
+}
+
+gboolean
+sp_capture_reader_skip (SpCaptureReader *self)
+{
+  SpCaptureFrame *frame;
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  if (!sp_capture_reader_ensure_space_for (self, sizeof (SpCaptureFrame)))
+    return FALSE;
+
+  frame = (SpCaptureFrame *)(gpointer)&self->buf[self->pos];
+  sp_capture_reader_bswap_frame (self, frame);
+
+  if (frame->len < sizeof (SpCaptureFrame))
+    return FALSE;
+
+  if (!sp_capture_reader_ensure_space_for (self, frame->len))
+    return FALSE;
+
+  frame = (SpCaptureFrame *)(gpointer)&self->buf[self->pos];
+
+  self->pos += frame->len;
+
+  if ((self->pos % SP_CAPTURE_ALIGN) != 0)
+    return FALSE;
+
+  return TRUE;
+}
+
+gboolean
+sp_capture_reader_peek_type (SpCaptureReader    *self,
+                             SpCaptureFrameType *type)
+{
+  SpCaptureFrame *frame;
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+  g_assert (self->pos <= self->bufsz);
+  g_assert (type != NULL);
+
+  *type = 0;
+
+  if (!sp_capture_reader_ensure_space_for (self, sizeof *frame))
+    return FALSE;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  frame = (SpCaptureFrame *)(gpointer)&self->buf[self->pos];
+
+  *type = frame->type;
+
+  return TRUE;
+}
+
+static const SpCaptureFrame *
+sp_capture_reader_read_basic (SpCaptureReader    *self,
+                              SpCaptureFrameType  type,
+                              gsize               extra)
+{
+  SpCaptureFrame *frame;
+  gsize len = sizeof *frame + extra;
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+  g_assert (self->pos <= self->bufsz);
+
+  if (!sp_capture_reader_ensure_space_for (self, len))
+    return NULL;
+
+  frame = (SpCaptureFrame *)(gpointer)&self->buf[self->pos];
+
+  sp_capture_reader_bswap_frame (self, frame);
+
+  if (frame->len < len)
+    return NULL;
+
+  if (frame->type != type)
+    return NULL;
+
+  self->pos += frame->len;
+
+  return frame;
+}
+
+const SpCaptureTimestamp *
+sp_capture_reader_read_timestamp (SpCaptureReader *self)
+{
+  return (SpCaptureTimestamp *)
+    sp_capture_reader_read_basic (self, SP_CAPTURE_FRAME_TIMESTAMP, 0);
+}
+
+const SpCaptureExit *
+sp_capture_reader_read_exit (SpCaptureReader *self)
+{
+  return (SpCaptureExit *)
+    sp_capture_reader_read_basic (self, SP_CAPTURE_FRAME_EXIT, 0);
+}
+
+const SpCaptureFork *
+sp_capture_reader_read_fork (SpCaptureReader *self)
+{
+  SpCaptureFork *fk;
+
+  g_assert (self != NULL);
+
+  fk = (SpCaptureFork *)
+    sp_capture_reader_read_basic (self, SP_CAPTURE_FRAME_FORK, sizeof(guint32));
+
+  if (fk != NULL)
+    {
+      if (G_UNLIKELY (self->endian != G_BYTE_ORDER))
+        fk->child_pid = GUINT32_SWAP_LE_BE (fk->child_pid);
+    }
+
+  return fk;
+}
+
+const SpCaptureMap *
+sp_capture_reader_read_map (SpCaptureReader *self)
+{
+  SpCaptureMap *map;
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+  g_assert (self->pos <= self->bufsz);
+
+  if (!sp_capture_reader_ensure_space_for (self, sizeof *map))
+    return NULL;
+
+  map = (SpCaptureMap *)(gpointer)&self->buf[self->pos];
+
+  sp_capture_reader_bswap_frame (self, &map->frame);
+
+  if (map->frame.type != SP_CAPTURE_FRAME_MAP)
+    return NULL;
+
+  if (map->frame.len < (sizeof *map + 1))
+    return NULL;
+
+  if (!sp_capture_reader_ensure_space_for (self, map->frame.len))
+    return NULL;
+
+  map = (SpCaptureMap *)(gpointer)&self->buf[self->pos];
+
+  if (self->buf[self->pos + map->frame.len - 1] != '\0')
+    return NULL;
+
+  sp_capture_reader_bswap_map (self, map);
+
+  self->pos += map->frame.len;
+
+  if ((self->pos % SP_CAPTURE_ALIGN) != 0)
+    return NULL;
+
+  return map;
+}
+
+const SpCaptureProcess *
+sp_capture_reader_read_process (SpCaptureReader *self)
+{
+  SpCaptureProcess *process;
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+  g_assert (self->pos <= self->bufsz);
+
+  if (!sp_capture_reader_ensure_space_for (self, sizeof *process))
+    return NULL;
+
+  process = (SpCaptureProcess *)(gpointer)&self->buf[self->pos];
+
+  sp_capture_reader_bswap_frame (self, &process->frame);
+
+  if (process->frame.type != SP_CAPTURE_FRAME_PROCESS)
+    return NULL;
+
+  if (process->frame.len < (sizeof *process + 1))
+    return NULL;
+
+  if (!sp_capture_reader_ensure_space_for (self, process->frame.len))
+    return NULL;
+
+  process = (SpCaptureProcess *)(gpointer)&self->buf[self->pos];
+
+  if (self->buf[self->pos + process->frame.len - 1] != '\0')
+    return NULL;
+
+  self->pos += process->frame.len;
+
+  if ((self->pos % SP_CAPTURE_ALIGN) != 0)
+    return NULL;
+
+  return process;
+}
+
+GHashTable *
+sp_capture_reader_read_jitmap (SpCaptureReader *self)
+{
+  g_autoptr(GHashTable) ret = NULL;
+  SpCaptureJitmap *jitmap;
+  guint8 *buf;
+  guint8 *endptr;
+  guint i;
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+  g_assert (self->pos <= self->bufsz);
+
+  if (!sp_capture_reader_ensure_space_for (self, sizeof *jitmap))
+    return NULL;
+
+  jitmap = (SpCaptureJitmap *)(gpointer)&self->buf[self->pos];
+
+  sp_capture_reader_bswap_frame (self, &jitmap->frame);
+
+  if (jitmap->frame.type != SP_CAPTURE_FRAME_JITMAP)
+    return NULL;
+
+  if (jitmap->frame.len < sizeof *jitmap)
+    return NULL;
+
+  if (!sp_capture_reader_ensure_space_for (self, jitmap->frame.len))
+    return NULL;
+
+  jitmap = (SpCaptureJitmap *)(gpointer)&self->buf[self->pos];
+
+  ret = g_hash_table_new_full (NULL, NULL, NULL, g_free);
+
+  buf = jitmap->data;
+  endptr = &self->buf[self->pos + jitmap->frame.len];
+
+  for (i = 0; i < jitmap->n_jitmaps; i++)
+    {
+      SpCaptureAddress addr;
+      const gchar *str;
+
+      if (buf + sizeof addr >= endptr)
+        return NULL;
+
+      memcpy (&addr, buf, sizeof addr);
+      buf += sizeof addr;
+
+      str = (gchar *)buf;
+
+      buf = memchr (buf, '\0', (endptr - buf));
+
+      if (buf == NULL)
+        return NULL;
+
+      buf++;
+
+      g_hash_table_insert (ret, GSIZE_TO_POINTER (addr), g_strdup (str));
+    }
+
+  self->pos += jitmap->frame.len;
+
+  return g_steal_pointer (&ret);
+}
+
+const SpCaptureSample *
+sp_capture_reader_read_sample (SpCaptureReader *self)
+{
+  SpCaptureSample *sample;
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+  g_assert (self->pos <= self->bufsz);
+
+  if (!sp_capture_reader_ensure_space_for (self, sizeof *sample))
+    return NULL;
+
+  sample = (SpCaptureSample *)(gpointer)&self->buf[self->pos];
+
+  sp_capture_reader_bswap_frame (self, &sample->frame);
+
+  if (sample->frame.type != SP_CAPTURE_FRAME_SAMPLE)
+    return NULL;
+
+  if (sample->frame.len < sizeof *sample)
+    return NULL;
+
+  if (self->endian != G_BYTE_ORDER)
+    sample->n_addrs = GUINT16_SWAP_LE_BE (sample->n_addrs);
+
+  if (sample->frame.len < (sizeof *sample + (sizeof(SpCaptureAddress) * sample->n_addrs)))
+    return NULL;
+
+  if (!sp_capture_reader_ensure_space_for (self, sample->frame.len))
+    return NULL;
+
+  sample = (SpCaptureSample *)(gpointer)&self->buf[self->pos];
+
+  if (self->endian != G_BYTE_ORDER)
+    {
+      guint i;
+
+      for (i = 0; i < sample->n_addrs; i++)
+        sample->addrs[i] = GUINT64_SWAP_LE_BE (sample->addrs[i]);
+    }
+
+  self->pos += sample->frame.len;
+
+  return sample;
+}
+
+gboolean
+sp_capture_reader_reset (SpCaptureReader *self)
+{
+  g_assert (self != NULL);
+
+  self->fd_off = sizeof (SpCaptureFileHeader);
+  self->pos = 0;
+  self->len = 0;
+
+  return TRUE;
+}
+
+SpCaptureReader *
+sp_capture_reader_ref (SpCaptureReader *self)
+{
+  g_assert (self != NULL);
+  g_assert (self->ref_count > 0);
+
+  g_atomic_int_inc (&self->ref_count);
+
+  return self;
+}
+
+void
+sp_capture_reader_unref (SpCaptureReader *self)
+{
+  g_assert (self != NULL);
+  g_assert (self->ref_count > 0);
+
+  if (g_atomic_int_dec_and_test (&self->ref_count))
+    sp_capture_reader_finalize (self);
+}
+
+gboolean
+sp_capture_reader_splice (SpCaptureReader  *self,
+                          SpCaptureWriter  *dest,
+                          GError          **error)
+{
+  g_assert (self != NULL);
+  g_assert (self->fd != -1);
+  g_assert (dest != NULL);
+
+  /* Flush before writing anything to ensure consistency */
+  if (!sp_capture_writer_flush (dest))
+    {
+      g_set_error (error,
+                   G_FILE_ERROR,
+                   g_file_error_from_errno (errno),
+                   "%s", g_strerror (errno));
+      return FALSE;
+    }
+
+  /*
+   * We don't need to track position because writer will
+   * track the current position to avoid reseting it.
+   */
+
+  /* Perform the splice */
+  return _sp_capture_writer_splice_from_fd (dest, self->fd, error);
+}
+
+/**
+ * sp_capture_reader_save_as:
+ * @self: An #SpCaptureReader
+ * @filename: the file to save the capture as
+ * @error: a location for a #GError or %NULL.
+ *
+ * This is a convenience function for copying a capture file for which
+ * you may have already discarded the writer for.
+ *
+ * Returns: %TRUE on success; otherwise %FALSE and @error is set.
+ */
+gboolean
+sp_capture_reader_save_as (SpCaptureReader  *self,
+                           const gchar      *filename,
+                           GError          **error)
+{
+  struct stat stbuf;
+  off_t in_off;
+  gsize to_write;
+  int fd = -1;
+
+  g_assert (self != NULL);
+  g_assert (filename != NULL);
+
+  if (-1 == (fd = open (filename, O_CREAT | O_WRONLY, 0640)))
+    goto handle_errno;
+
+  if (-1 == fstat (self->fd, &stbuf))
+    goto handle_errno;
+
+  if (-1 == ftruncate (fd, stbuf.st_size))
+    goto handle_errno;
+
+  if ((off_t)-1 == lseek (fd, 0L, SEEK_SET))
+    goto handle_errno;
+
+  in_off = 0;
+  to_write = stbuf.st_size;
+
+  while (to_write > 0)
+    {
+      gssize written;
+
+      written = sendfile (fd, self->fd, &in_off, to_write);
+
+      if (written < 0)
+        goto handle_errno;
+
+      if (written == 0 && errno != EAGAIN)
+        goto handle_errno;
+
+      g_assert (written <= (gssize)to_write);
+
+      to_write -= written;
+    }
+
+  close (fd);
+
+  return TRUE;
+
+handle_errno:
+  if (fd != -1)
+    close (fd);
+
+  g_set_error (error,
+               G_FILE_ERROR,
+               g_file_error_from_errno (errno),
+               "%s", g_strerror (errno));
+
+  return FALSE;
+}
diff --git a/util/sp-capture-reader.h b/util/sp-capture-reader.h
new file mode 100644
index 0000000..14d93c8
--- /dev/null
+++ b/util/sp-capture-reader.h
@@ -0,0 +1,65 @@
+/* sp-capture-reader.h
+ *
+ * Copyright (C) 2016 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 2 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 SP_CAPTURE_READER_H
+#define SP_CAPTURE_READER_H
+
+#include "sp-capture-types.h"
+
+G_BEGIN_DECLS
+
+typedef struct _SpCaptureReader SpCaptureReader;
+
+SpCaptureReader          *sp_capture_reader_new            (const gchar         *filename,
+                                                            GError             **error);
+SpCaptureReader          *sp_capture_reader_new_from_fd    (int                  fd,
+                                                            GError             **error);
+SpCaptureReader          *sp_capture_reader_ref            (SpCaptureReader     *self);
+void                      sp_capture_reader_unref          (SpCaptureReader     *self);
+const gchar              *sp_capture_reader_get_filename   (SpCaptureReader     *self);
+const gchar              *sp_capture_reader_get_time       (SpCaptureReader     *self);
+gboolean                  sp_capture_reader_skip           (SpCaptureReader     *self);
+gboolean                  sp_capture_reader_peek_type      (SpCaptureReader     *self,
+                                                            SpCaptureFrameType  *type);
+const SpCaptureMap       *sp_capture_reader_read_map       (SpCaptureReader     *self);
+const SpCaptureExit      *sp_capture_reader_read_exit      (SpCaptureReader     *self);
+const SpCaptureFork      *sp_capture_reader_read_fork      (SpCaptureReader     *self);
+const SpCaptureTimestamp *sp_capture_reader_read_timestamp (SpCaptureReader     *self);
+const SpCaptureProcess   *sp_capture_reader_read_process   (SpCaptureReader     *self);
+const SpCaptureSample    *sp_capture_reader_read_sample    (SpCaptureReader     *self);
+GHashTable               *sp_capture_reader_read_jitmap    (SpCaptureReader     *self);
+gboolean                  sp_capture_reader_reset          (SpCaptureReader     *self);
+gboolean                  sp_capture_reader_splice         (SpCaptureReader     *self,
+                                                            SpCaptureWriter     *dest,
+                                                            GError             **error);
+gboolean                  sp_capture_reader_save_as        (SpCaptureReader     *self,
+                                                            const gchar         *filename,
+                                                            GError             **error);
+
+#ifndef SP_DISABLE_GOBJECT
+# define SP_TYPE_CAPTURE_READER (sp_capture_reader_get_type())
+  GType sp_capture_reader_get_type (void);
+#endif
+
+#if GLIB_CHECK_VERSION(2, 44, 0)
+  G_DEFINE_AUTOPTR_CLEANUP_FUNC (SpCaptureReader, sp_capture_reader_unref)
+#endif
+
+G_END_DECLS
+
+#endif /* SP_CAPTURE_READER_H */
diff --git a/util/sp-capture-types.h b/util/sp-capture-types.h
new file mode 100644
index 0000000..7bb3ab7
--- /dev/null
+++ b/util/sp-capture-types.h
@@ -0,0 +1,155 @@
+/* sp-capture-types.h
+ *
+ * Copyright (C) 2016 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 2 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 SP_CAPTURE_FORMAT_H
+#define SP_CAPTURE_FORMAT_H
+
+#include <glib.h>
+
+#ifndef SP_DISABLE_GOBJECT
+# include <glib-object.h>
+#endif
+
+G_BEGIN_DECLS
+
+#define SP_CAPTURE_MAGIC (GUINT32_TO_LE(0xFDCA975E))
+#define SP_CAPTURE_ALIGN (sizeof(SpCaptureAddress))
+
+#if __WORDSIZE == 64
+# define SP_CAPTURE_JITMAP_MARK    G_GUINT64_CONSTANT(0xE000000000000000)
+# define SP_CAPTURE_ADDRESS_FORMAT "0x%016lx"
+#else
+# define SP_CAPTURE_JITMAP_MARK    G_GUINT64_CONSTANT(0xE0000000)
+# define SP_CAPTURE_ADDRESS_FORMAT "0x%016llx"
+#endif
+
+#define SP_CAPTURE_CURRENT_TIME (g_get_monotonic_time() * 1000L)
+
+typedef struct _SpCaptureReader SpCaptureReader;
+typedef struct _SpCaptureWriter SpCaptureWriter;
+
+typedef guint64 SpCaptureAddress;
+
+typedef enum
+{
+  SP_CAPTURE_FRAME_TIMESTAMP = 1,
+  SP_CAPTURE_FRAME_SAMPLE    = 2,
+  SP_CAPTURE_FRAME_MAP       = 3,
+  SP_CAPTURE_FRAME_PROCESS   = 4,
+  SP_CAPTURE_FRAME_FORK      = 5,
+  SP_CAPTURE_FRAME_EXIT      = 6,
+  SP_CAPTURE_FRAME_JITMAP    = 7,
+} SpCaptureFrameType;
+
+#pragma pack(push, 1)
+
+typedef struct
+{
+  guint32 magic;
+  guint8  version;
+  guint32 little_endian : 1;
+  guint32 padding : 23;
+  gchar   capture_time[64];
+  gchar   suffix[184];
+} SpCaptureFileHeader;
+
+typedef struct
+{
+  guint16 len;
+  gint16  cpu;
+  gint32  pid;
+  gint64  time;
+  guint8  type;
+  guint64 padding : 56;
+  guint8  data[0];
+} SpCaptureFrame;
+
+typedef struct
+{
+  SpCaptureFrame frame;
+  guint64        start;
+  guint64        end;
+  guint64        offset;
+  guint64        inode;
+  gchar          filename[0];
+} SpCaptureMap;
+
+typedef struct
+{
+  SpCaptureFrame frame;
+  guint32        n_jitmaps;
+  guint8         data[0];
+} SpCaptureJitmap;
+
+typedef struct
+{
+  SpCaptureFrame frame;
+  gchar          cmdline[0];
+} SpCaptureProcess;
+
+typedef struct
+{
+  SpCaptureFrame   frame;
+  guint16          n_addrs;
+  guint64          padding : 48;
+  SpCaptureAddress addrs[0];
+} SpCaptureSample;
+
+typedef struct
+{
+  SpCaptureFrame frame;
+  GPid           child_pid;
+} SpCaptureFork;
+
+typedef struct
+{
+  SpCaptureFrame frame;
+} SpCaptureExit;
+
+typedef struct
+{
+  SpCaptureFrame frame;
+} SpCaptureTimestamp;
+
+#pragma pack(pop)
+
+G_STATIC_ASSERT (sizeof (SpCaptureFileHeader) == 256);
+G_STATIC_ASSERT (sizeof (SpCaptureFrame) == 24);
+G_STATIC_ASSERT (sizeof (SpCaptureMap) == 56);
+G_STATIC_ASSERT (sizeof (SpCaptureJitmap) == 28);
+G_STATIC_ASSERT (sizeof (SpCaptureProcess) == 24);
+G_STATIC_ASSERT (sizeof (SpCaptureSample) == 32);
+G_STATIC_ASSERT (sizeof (SpCaptureFork) == 28);
+G_STATIC_ASSERT (sizeof (SpCaptureExit) == 24);
+G_STATIC_ASSERT (sizeof (SpCaptureTimestamp) == 24);
+
+static inline gint
+sp_capture_address_compare (SpCaptureAddress a,
+                            SpCaptureAddress b)
+{
+  if (a < b)
+    return -1;
+  if (a > b)
+    return 1;
+  else
+    return 0;
+}
+
+G_END_DECLS
+
+#endif /* SP_CAPTURE_FORMAT_H */
diff --git a/util/sp-capture-writer.c b/util/sp-capture-writer.c
new file mode 100644
index 0000000..f0f725f
--- /dev/null
+++ b/util/sp-capture-writer.c
@@ -0,0 +1,954 @@
+/* sp-capture-writer.c
+ *
+ * Copyright (C) 2016 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 2 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 _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+
+#include <errno.h>
+#include <fcntl.h>
+#include <glib/gstdio.h>
+#include <string.h>
+#include <sys/sendfile.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "sp-capture-reader.h"
+#include "sp-capture-writer.h"
+
+#define DEFAULT_BUFFER_SIZE (getpagesize() * 64L)
+#define INVALID_ADDRESS     (G_GUINT64_CONSTANT(0))
+
+typedef struct
+{
+  /* A pinter into the string buffer */
+  const gchar *str;
+
+  /* The unique address for the string */
+  guint64 addr;
+} SpCaptureJitmapBucket;
+
+struct _SpCaptureWriter
+{
+  /*
+   * This is our buffer location for incoming strings. This is used
+   * similarly to GStringChunk except there is only one-page, and after
+   * it fills, we flush to disk.
+   *
+   * This is paired with a closed hash table for deduplication.
+   */
+  gchar addr_buf[4096*4];
+
+  /* Our hashtable for deduplication. */
+  SpCaptureJitmapBucket addr_hash[512];
+
+  /* We keep the large fields above so that our allocation will be page
+   * alinged for the write buffer. This improves the performance of large
+   * writes to the target file-descriptor.
+   */
+
+  volatile gint ref_count;
+
+  /*
+   * Our address sequence counter. The value that comes from
+   * monotonically increasing this is OR'd with JITMAP_MARK to denote
+   * the address name should come from the JIT map.
+   */
+  gsize addr_seq;
+
+  /* Our position in addr_buf. */
+  gsize addr_buf_pos;
+
+  /*
+   * The number of hash table items in @addr_hash. This is an
+   * optimization so that we can avoid calculating the number of strings
+   * when flushing out the jitmap.
+   */
+  guint addr_hash_size;
+
+  /* Capture file handle */
+  int fd;
+
+  /* Our write buffer for fd */
+  guint8 *buf;
+  gsize pos;
+  gsize len;
+
+  /* Statistics while recording */
+  SpCaptureStat stat;
+};
+
+#ifndef SP_DISABLE_GOBJECT
+G_DEFINE_BOXED_TYPE (SpCaptureWriter, sp_capture_writer,
+                     sp_capture_writer_ref, sp_capture_writer_unref)
+#endif
+
+static void
+sp_capture_writer_finalize (SpCaptureWriter *self)
+{
+  if (self != NULL)
+    {
+      sp_capture_writer_flush (self);
+      close (self->fd);
+      g_free (self->buf);
+      g_free (self);
+    }
+}
+
+SpCaptureWriter *
+sp_capture_writer_ref (SpCaptureWriter *self)
+{
+  g_assert (self != NULL);
+  g_assert (self->ref_count > 0);
+
+  g_atomic_int_inc (&self->ref_count);
+
+  return self;
+}
+
+void
+sp_capture_writer_unref (SpCaptureWriter *self)
+{
+  g_assert (self != NULL);
+  g_assert (self->ref_count > 0);
+
+  if (g_atomic_int_dec_and_test (&self->ref_count))
+    sp_capture_writer_finalize (self);
+}
+
+static gboolean
+sp_capture_writer_flush_data (SpCaptureWriter *self)
+{
+  const guint8 *buf;
+  gssize written;
+  gsize to_write;
+
+  g_assert (self != NULL);
+  g_assert (self->pos <= self->len);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  buf = self->buf;
+  to_write = self->pos;
+
+  while (to_write > 0)
+    {
+      written = write (self->fd, buf, to_write);
+      if (written < 0)
+        return FALSE;
+
+      if (written == 0 && errno != EAGAIN)
+        return FALSE;
+
+      g_assert (written <= (gssize)to_write);
+
+      buf += written;
+      to_write -= written;
+    }
+
+  self->pos = 0;
+
+  return TRUE;
+}
+
+static inline void
+sp_capture_writer_realign (gsize *pos)
+{
+  *pos = (*pos + SP_CAPTURE_ALIGN - 1) & ~(SP_CAPTURE_ALIGN - 1);
+}
+
+static inline gboolean
+sp_capture_writer_ensure_space_for (SpCaptureWriter *self,
+                                    gsize            len)
+{
+  if ((self->len - self->pos) < len)
+    {
+      if (!sp_capture_writer_flush_data (self))
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+static gboolean
+sp_capture_writer_flush_jitmap (SpCaptureWriter *self)
+{
+  SpCaptureJitmap jitmap;
+  gssize r;
+  gsize len;
+
+  g_assert (self != NULL);
+
+  if (self->addr_hash_size == 0)
+    return TRUE;
+
+  g_assert (self->addr_buf_pos > 0);
+
+  len = sizeof jitmap + self->addr_buf_pos;
+
+  sp_capture_writer_realign (&len);
+
+  jitmap.frame.len = len;
+  jitmap.frame.cpu = -1;
+  jitmap.frame.pid = getpid ();
+  jitmap.frame.time = g_get_monotonic_time ();
+  jitmap.frame.type = SP_CAPTURE_FRAME_JITMAP;
+  jitmap.n_jitmaps = self->addr_hash_size;
+
+  if (sizeof jitmap != write (self->fd, &jitmap, sizeof jitmap))
+    return FALSE;
+
+  r = write (self->fd, self->addr_buf, len - sizeof jitmap);
+  if (r < 0 || (gsize)r != (len - sizeof jitmap))
+    return FALSE;
+
+  self->addr_buf_pos = 0;
+  self->addr_hash_size = 0;
+  memset (self->addr_hash, 0, sizeof self->addr_hash);
+
+  self->stat.frame_count[SP_CAPTURE_FRAME_JITMAP]++;
+
+  return TRUE;
+}
+
+static gboolean
+sp_capture_writer_lookup_jitmap (SpCaptureWriter  *self,
+                                 const gchar      *name,
+                                 SpCaptureAddress *addr)
+{
+  guint hash;
+  guint i;
+
+  g_assert (self != NULL);
+  g_assert (name != NULL);
+  g_assert (addr != NULL);
+
+  hash = g_str_hash (name) % G_N_ELEMENTS (self->addr_hash);
+
+  for (i = hash; i < G_N_ELEMENTS (self->addr_hash); i++)
+    {
+      SpCaptureJitmapBucket *bucket = &self->addr_hash[i];
+
+      if (bucket->str == NULL)
+        return FALSE;
+
+      if (strcmp (bucket->str, name) == 0)
+        {
+          *addr = bucket->addr;
+          return TRUE;
+        }
+    }
+
+  for (i = 0; i < hash; i++)
+    {
+      SpCaptureJitmapBucket *bucket = &self->addr_hash[i];
+
+      if (bucket->str == NULL)
+        return FALSE;
+
+      if (strcmp (bucket->str, name) == 0)
+        {
+          *addr = bucket->addr;
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+static SpCaptureAddress
+sp_capture_writer_insert_jitmap (SpCaptureWriter *self,
+                                 const gchar     *str)
+{
+  SpCaptureAddress addr;
+  gchar *dst;
+  gsize len;
+  guint hash;
+  guint i;
+
+  g_assert (self != NULL);
+  g_assert (str != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  len = sizeof addr + strlen (str) + 1;
+
+  if ((self->addr_hash_size == G_N_ELEMENTS (self->addr_hash)) ||
+      ((sizeof self->addr_buf - self->addr_buf_pos) < len))
+    {
+      if (!sp_capture_writer_flush_jitmap (self))
+        return INVALID_ADDRESS;
+
+      g_assert (self->addr_hash_size == 0);
+      g_assert (self->addr_buf_pos == 0);
+    }
+
+  g_assert (self->addr_hash_size < G_N_ELEMENTS (self->addr_hash));
+  g_assert (len > sizeof addr);
+
+  /* Allocate the next unique address */
+  addr = SP_CAPTURE_JITMAP_MARK | ++self->addr_seq;
+
+  /* Copy the address into the buffer */
+  dst = (gchar *)&self->addr_buf[self->addr_buf_pos];
+  memcpy (dst, &addr, sizeof addr);
+
+  /*
+   * Copy the string into the buffer, keeping dst around for
+   * when we insert into the hashtable.
+   */
+  dst += sizeof addr;
+  memcpy (dst, str, len - sizeof addr);
+
+  /* Advance our string cache position */
+  self->addr_buf_pos += len;
+  g_assert (self->addr_buf_pos <= sizeof self->addr_buf);
+
+  /* Now place the address into the hashtable */
+  hash = g_str_hash (str) % G_N_ELEMENTS (self->addr_hash);
+
+  /* Start from the current hash bucket and go forward */
+  for (i = hash; i < G_N_ELEMENTS (self->addr_hash); i++)
+    {
+      SpCaptureJitmapBucket *bucket = &self->addr_hash[i];
+
+      if (G_LIKELY (bucket->str == NULL))
+        {
+          bucket->str = dst;
+          bucket->addr = addr;
+          self->addr_hash_size++;
+          return addr;
+        }
+    }
+
+  /* Wrap around to the beginning */
+  for (i = 0; i < hash; i++)
+    {
+      SpCaptureJitmapBucket *bucket = &self->addr_hash[i];
+
+      if (G_LIKELY (bucket->str == NULL))
+        {
+          bucket->str = dst;
+          bucket->addr = addr;
+          self->addr_hash_size++;
+          return addr;
+        }
+    }
+
+  g_assert_not_reached ();
+
+  return INVALID_ADDRESS;
+}
+
+SpCaptureWriter *
+sp_capture_writer_new_from_fd (int   fd,
+                               gsize buffer_size)
+{
+  g_autofree gchar *nowstr = NULL;
+  SpCaptureWriter *self;
+  SpCaptureFileHeader *header;
+  GTimeVal tv;
+
+  if (buffer_size == 0)
+    buffer_size = DEFAULT_BUFFER_SIZE;
+
+  g_assert (fd != -1);
+  g_assert (buffer_size % getpagesize() == 0);
+
+  self = g_new0 (SpCaptureWriter, 1);
+  self->ref_count = 1;
+  self->fd = fd;
+  self->buf = (guint8 *)g_malloc (buffer_size);
+  self->len = buffer_size;
+
+  g_get_current_time (&tv);
+  nowstr = g_time_val_to_iso8601 (&tv);
+
+  header = (SpCaptureFileHeader *)(gpointer)self->buf;
+  header->magic = SP_CAPTURE_MAGIC;
+  header->version = 1;
+#ifdef G_LITTLE_ENDIAN
+  header->little_endian = TRUE;
+#else
+  header->little_endian = FALSE;
+#endif
+  header->padding = 0;
+  g_strlcpy (header->capture_time, nowstr, sizeof header->capture_time);
+  memset (header->suffix, 0, sizeof header->suffix);
+
+  self->pos += sizeof *header;
+
+  if (!sp_capture_writer_flush_data (self))
+    {
+      sp_capture_writer_finalize (self);
+      return NULL;
+    }
+
+  g_assert (self->pos == 0);
+  g_assert (self->len > 0);
+  g_assert (self->len % getpagesize() == 0);
+  g_assert (self->buf != NULL);
+  g_assert (self->addr_hash_size == 0);
+  g_assert (self->fd != -1);
+
+  return self;
+}
+
+SpCaptureWriter *
+sp_capture_writer_new (const gchar *filename,
+                       gsize        buffer_size)
+{
+  SpCaptureWriter *self;
+  int fd;
+
+  g_assert (filename != NULL);
+  g_assert (buffer_size % getpagesize() == 0);
+
+  if ((-1 == (fd = open (filename, O_CREAT | O_RDWR, 0640))) ||
+      (-1 == ftruncate (fd, 0L)))
+    return NULL;
+
+  self = sp_capture_writer_new_from_fd (fd, buffer_size);
+
+  if (self == NULL)
+    close (fd);
+
+  return self;
+}
+
+gboolean
+sp_capture_writer_add_map (SpCaptureWriter *self,
+                           gint64           time,
+                           gint             cpu,
+                           GPid             pid,
+                           guint64          start,
+                           guint64          end,
+                           guint64          offset,
+                           guint64          inode,
+                           const gchar     *filename)
+{
+  SpCaptureMap *ev;
+  gsize len;
+
+  if (filename == NULL)
+    filename = "";
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+  g_assert (filename != NULL);
+
+  len = sizeof *ev + strlen (filename) + 1;
+
+  sp_capture_writer_realign (&len);
+
+  if (!sp_capture_writer_ensure_space_for (self, len))
+    return FALSE;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  ev = (SpCaptureMap *)(gpointer)&self->buf[self->pos];
+  ev->frame.len = len;
+  ev->frame.cpu = cpu;
+  ev->frame.pid = pid;
+  ev->frame.time = time;
+  ev->frame.type = SP_CAPTURE_FRAME_MAP;
+  ev->frame.padding = 0;
+  ev->start = start;
+  ev->end = end;
+  ev->offset = offset;
+  ev->inode = inode;
+
+  g_strlcpy (ev->filename, filename, len - sizeof *ev);
+  ev->filename[len - sizeof *ev - 1] = '\0';
+
+  self->pos += ev->frame.len;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  self->stat.frame_count[SP_CAPTURE_FRAME_MAP]++;
+
+  return TRUE;
+}
+
+SpCaptureAddress
+sp_capture_writer_add_jitmap (SpCaptureWriter *self,
+                              const gchar     *name)
+{
+  SpCaptureAddress addr = INVALID_ADDRESS;
+
+  if (name == NULL)
+    name = "";
+
+  g_assert (self != NULL);
+  g_assert (name != NULL);
+
+  if (!sp_capture_writer_lookup_jitmap (self, name, &addr))
+    addr = sp_capture_writer_insert_jitmap (self, name);
+
+  return addr;
+}
+
+gboolean
+sp_capture_writer_add_process (SpCaptureWriter *self,
+                               gint64           time,
+                               gint             cpu,
+                               GPid             pid,
+                               const gchar     *cmdline)
+{
+  SpCaptureProcess *ev;
+  gsize len;
+
+  if (cmdline == NULL)
+    cmdline = "";
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+  g_assert (cmdline != NULL);
+
+  len = sizeof *ev + strlen (cmdline) + 1;
+
+  sp_capture_writer_realign (&len);
+
+  if (!sp_capture_writer_ensure_space_for (self, len))
+    return FALSE;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  ev = (SpCaptureProcess *)(gpointer)&self->buf[self->pos];
+  ev->frame.len = len;
+  ev->frame.cpu = cpu;
+  ev->frame.pid = pid;
+  ev->frame.time = time;
+  ev->frame.type = SP_CAPTURE_FRAME_PROCESS;
+  ev->frame.padding = 0;
+
+  g_strlcpy (ev->cmdline, cmdline, len - sizeof *ev);
+  ev->cmdline[len - sizeof *ev - 1] = '\0';
+
+  self->pos += ev->frame.len;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  self->stat.frame_count[SP_CAPTURE_FRAME_PROCESS]++;
+
+  return TRUE;
+}
+
+gboolean
+sp_capture_writer_add_sample (SpCaptureWriter        *self,
+                              gint64                  time,
+                              gint                    cpu,
+                              GPid                    pid,
+                              const SpCaptureAddress *addrs,
+                              guint                   n_addrs)
+{
+  SpCaptureSample *ev;
+  gsize len;
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  len = sizeof *ev + (n_addrs * sizeof (SpCaptureAddress));
+
+  sp_capture_writer_realign (&len);
+
+  if (!sp_capture_writer_ensure_space_for (self, len))
+    return FALSE;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  ev = (SpCaptureSample *)(gpointer)&self->buf[self->pos];
+  ev->frame.len = len;
+  ev->frame.cpu = cpu;
+  ev->frame.pid = pid;
+  ev->frame.time = time;
+  ev->frame.type = SP_CAPTURE_FRAME_SAMPLE;
+  ev->frame.padding = 0;
+  ev->n_addrs = n_addrs;
+
+  memcpy (ev->addrs, addrs, (n_addrs * sizeof (SpCaptureAddress)));
+
+  self->pos += ev->frame.len;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  self->stat.frame_count[SP_CAPTURE_FRAME_SAMPLE]++;
+
+  return TRUE;
+}
+
+gboolean
+sp_capture_writer_add_fork (SpCaptureWriter *self,
+                            gint64           time,
+                            gint             cpu,
+                            GPid             pid,
+                            GPid             child_pid)
+{
+  SpCaptureFork *ev;
+  gsize len = sizeof *ev;
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  sp_capture_writer_realign (&len);
+
+  if (!sp_capture_writer_ensure_space_for (self, len))
+    return FALSE;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  ev = (SpCaptureFork *)(gpointer)&self->buf[self->pos];
+  ev->frame.len = len;
+  ev->frame.cpu = cpu;
+  ev->frame.pid = pid;
+  ev->frame.time = time;
+  ev->frame.type = SP_CAPTURE_FRAME_FORK;
+  ev->frame.padding = 0;
+  ev->child_pid = child_pid;
+
+  self->pos += ev->frame.len;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  self->stat.frame_count[SP_CAPTURE_FRAME_FORK]++;
+
+  return TRUE;
+}
+
+gboolean
+sp_capture_writer_add_exit (SpCaptureWriter *self,
+                            gint64           time,
+                            gint             cpu,
+                            GPid             pid)
+{
+  SpCaptureExit *ev;
+  gsize len = sizeof *ev;
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  sp_capture_writer_realign (&len);
+
+  if (!sp_capture_writer_ensure_space_for (self, len))
+    return FALSE;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  ev = (SpCaptureExit *)(gpointer)&self->buf[self->pos];
+  ev->frame.len = len;
+  ev->frame.cpu = cpu;
+  ev->frame.pid = pid;
+  ev->frame.time = time;
+  ev->frame.type = SP_CAPTURE_FRAME_EXIT;
+  ev->frame.padding = 0;
+
+  self->pos += ev->frame.len;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  self->stat.frame_count[SP_CAPTURE_FRAME_EXIT]++;
+
+  return TRUE;
+}
+
+gboolean
+sp_capture_writer_add_timestamp (SpCaptureWriter *self,
+                                 gint64           time,
+                                 gint             cpu,
+                                 GPid             pid)
+{
+  SpCaptureTimestamp *ev;
+  gsize len = sizeof *ev;
+
+  g_assert (self != NULL);
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  sp_capture_writer_realign (&len);
+
+  if (!sp_capture_writer_ensure_space_for (self, len))
+    return FALSE;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  ev = (SpCaptureTimestamp *)(gpointer)&self->buf[self->pos];
+  ev->frame.len = len;
+  ev->frame.cpu = cpu;
+  ev->frame.pid = pid;
+  ev->frame.time = time;
+  ev->frame.type = SP_CAPTURE_FRAME_TIMESTAMP;
+  ev->frame.padding = 0;
+
+  self->pos += ev->frame.len;
+
+  g_assert ((self->pos % SP_CAPTURE_ALIGN) == 0);
+
+  self->stat.frame_count[SP_CAPTURE_FRAME_TIMESTAMP]++;
+
+  return TRUE;
+}
+
+gboolean
+sp_capture_writer_flush (SpCaptureWriter *self)
+{
+  g_assert (self != NULL);
+
+  return (sp_capture_writer_flush_jitmap (self) &&
+          sp_capture_writer_flush_data (self));
+}
+
+/**
+ * sp_capture_writer_save_as:
+ * @self: A #SpCaptureWriter
+ * @filename: the file to save the capture as
+ * @error: a location for a #GError or %NULL.
+ *
+ * Saves the captured data as the file @filename.
+ *
+ * This is primarily useful if the writer was created with a memory-backed
+ * file-descriptor such as a memfd or tmpfs file on Linux.
+ *
+ * Returns: %TRUE if successful, otherwise %FALSE and @error is set.
+ */
+gboolean
+sp_capture_writer_save_as (SpCaptureWriter            *self,
+                           const gchar                *filename,
+                           GError                    **error)
+{
+  gsize to_write;
+  off_t in_off;
+  off_t pos;
+  int fd = -1;
+
+  g_assert (self != NULL);
+  g_assert (self->fd != -1);
+  g_assert (filename != NULL);
+
+  if (-1 == (fd = open (filename, O_CREAT | O_RDWR, 0640)))
+    goto handle_errno;
+
+  if (!sp_capture_writer_flush (self))
+    goto handle_errno;
+
+  if (-1 == (pos = lseek (self->fd, 0L, SEEK_CUR)))
+    goto handle_errno;
+
+  to_write = pos;
+  in_off = 0;
+
+  while (to_write > 0)
+    {
+      gssize written;
+
+      written = sendfile (fd, self->fd, &in_off, pos);
+
+      if (written < 0)
+        goto handle_errno;
+
+      if (written == 0 && errno != EAGAIN)
+        goto handle_errno;
+
+      g_assert (written <= (gssize)to_write);
+
+      to_write -= written;
+    }
+
+  close (fd);
+
+  return TRUE;
+
+handle_errno:
+  g_set_error (error,
+               G_FILE_ERROR,
+               g_file_error_from_errno (errno),
+               "%s", g_strerror (errno));
+
+  if (fd != -1)
+    {
+      close (fd);
+      g_unlink (filename);
+    }
+
+  return FALSE;
+}
+
+/**
+ * _sp_capture_writer_splice_from_fd:
+ * @self: An #SpCaptureWriter
+ * @fd: the fd to read from.
+ * @error: A location for a #GError, or %NULL.
+ *
+ * This is internal API for SpCaptureWriter and SpCaptureReader to
+ * communicate when splicing a reader into a writer.
+ *
+ * This should not be used outside of #SpCaptureReader or
+ * #SpCaptureWriter.
+ *
+ * This will not advance the position of @fd.
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ */
+gboolean
+_sp_capture_writer_splice_from_fd (SpCaptureWriter  *self,
+                                   int               fd,
+                                   GError          **error)
+{
+  struct stat stbuf;
+  off_t in_off;
+  gsize to_write;
+
+  g_assert (self != NULL);
+  g_assert (self->fd != -1);
+
+  if (-1 == fstat (fd, &stbuf))
+    goto handle_errno;
+
+  if (stbuf.st_size < 256)
+    {
+      g_set_error (error,
+                   G_FILE_ERROR,
+                   G_FILE_ERROR_INVAL,
+                   "Cannot splice, possibly corrupt file.");
+      return FALSE;
+    }
+
+  in_off = 256;
+  to_write = stbuf.st_size - in_off;
+
+  while (to_write > 0)
+    {
+      gssize written;
+
+      written = sendfile (self->fd, fd, &in_off, to_write);
+
+      if (written < 0)
+        goto handle_errno;
+
+      if (written == 0 && errno != EAGAIN)
+        goto handle_errno;
+
+      g_assert (written <= (gssize)to_write);
+
+      to_write -= written;
+    }
+
+  return TRUE;
+
+handle_errno:
+  g_set_error (error,
+               G_FILE_ERROR,
+               g_file_error_from_errno (errno),
+               "%s", g_strerror (errno));
+
+  return FALSE;
+}
+
+/**
+ * sp_capture_writer_splice:
+ * @self: An #SpCaptureWriter
+ * @dest: An #SpCaptureWriter
+ * @error: A location for a #GError, or %NULL.
+ *
+ * This function will copy the capture @self into the capture @dest.  This
+ * tries to be semi-efficient by using sendfile() to copy the contents between
+ * the captures. @self and @dest will be flushed before the contents are copied
+ * into the @dest file-descriptor.
+ *
+ * Returns: %TRUE if successful, otherwise %FALSE and and @error is set.
+ */
+gboolean
+sp_capture_writer_splice (SpCaptureWriter  *self,
+                          SpCaptureWriter  *dest,
+                          GError          **error)
+{
+  gboolean ret;
+  off_t pos;
+
+  g_assert (self != NULL);
+  g_assert (self->fd != -1);
+  g_assert (dest != NULL);
+  g_assert (dest->fd != -1);
+
+  /* Flush before writing anything to ensure consistency */
+  if (!sp_capture_writer_flush (self) || !sp_capture_writer_flush (dest))
+    goto handle_errno;
+
+  /* Track our current position so we can reset */
+  if ((off_t)-1 == (pos = lseek (self->fd, 0L, SEEK_CUR)))
+    goto handle_errno;
+
+  /* Perform the splice */
+  ret = _sp_capture_writer_splice_from_fd (dest, self->fd, error);
+
+  /* Now reset or file-descriptor position (it should be the same */
+  if (pos != lseek (self->fd, pos, SEEK_SET))
+    {
+      ret = FALSE;
+      goto handle_errno;
+    }
+
+  return ret;
+
+handle_errno:
+  g_set_error (error,
+               G_FILE_ERROR,
+               g_file_error_from_errno (errno),
+               "%s", g_strerror (errno));
+
+  return FALSE;
+}
+
+/**
+ * sp_capture_writer_create_reader:
+ * @self: A #SpCaptureWriter
+ * @error: a location for a #GError, or %NULL
+ *
+ * Creates a new reader for the writer.
+ *
+ * Since readers use positioned reads, this uses the same file-descriptor for
+ * the #SpCaptureReader. Therefore, if you are writing to the capture while
+ * also consuming from the reader, you could get transient failures unless you
+ * synchronize the operations.
+ *
+ * Returns: (transfer full): A #SpCaptureReader.
+ */
+SpCaptureReader *
+sp_capture_writer_create_reader (SpCaptureWriter  *self,
+                                 GError          **error)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->fd != -1, NULL);
+
+  return sp_capture_reader_new_from_fd (self->fd, error);
+}
+
+/**
+ * sp_capture_writer_stat:
+ * @self: A #SpCaptureWriter
+ * @stat: (out): A location for an #SpCaptureStat
+ *
+ * This function will fill @stat with statistics generated while capturing
+ * the profiler session.
+ */
+void
+sp_capture_writer_stat (SpCaptureWriter *self,
+                        SpCaptureStat   *stat)
+{
+  g_return_if_fail (self != NULL);
+  g_return_if_fail (stat != NULL);
+
+  *stat = self->stat;
+}
diff --git a/util/sp-capture-writer.h b/util/sp-capture-writer.h
new file mode 100644
index 0000000..1846fd1
--- /dev/null
+++ b/util/sp-capture-writer.h
@@ -0,0 +1,108 @@
+/* sp-capture-writer.h
+ *
+ * Copyright (C) 2016 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 2 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 SP_CAPTURE_WRITER_H
+#define SP_CAPTURE_WRITER_H
+
+#include "sp-capture-types.h"
+
+G_BEGIN_DECLS
+
+typedef struct _SpCaptureWriter SpCaptureWriter;
+
+typedef struct
+{
+  /*
+   * The number of frames indexed by SpCaptureFrameType
+   */
+  gsize frame_count[16];
+
+  /*
+   * Padding for future expansion.
+   */
+  gsize padding[48];
+} SpCaptureStat;
+
+SpCaptureWriter    *sp_capture_writer_new             (const gchar             *filename,
+                                                       gsize                    buffer_size);
+SpCaptureWriter    *sp_capture_writer_new_from_fd     (int                      fd,
+                                                       gsize                    buffer_size);
+SpCaptureWriter    *sp_capture_writer_ref             (SpCaptureWriter         *self);
+void                sp_capture_writer_unref           (SpCaptureWriter         *self);
+void                sp_capture_writer_stat            (SpCaptureWriter         *self,
+                                                       SpCaptureStat           *stat);
+gboolean            sp_capture_writer_add_map         (SpCaptureWriter         *self,
+                                                       gint64                   time,
+                                                       gint                     cpu,
+                                                       GPid                     pid,
+                                                       guint64                  start,
+                                                       guint64                  end,
+                                                       guint64                  offset,
+                                                       guint64                  inode,
+                                                       const gchar             *filename);
+guint64             sp_capture_writer_add_jitmap      (SpCaptureWriter         *self,
+                                                       const gchar             *name);
+gboolean            sp_capture_writer_add_process     (SpCaptureWriter         *self,
+                                                       gint64                   time,
+                                                       gint                     cpu,
+                                                       GPid                     pid,
+                                                       const gchar             *cmdline);
+gboolean            sp_capture_writer_add_sample      (SpCaptureWriter         *self,
+                                                       gint64                   time,
+                                                       gint                     cpu,
+                                                       GPid                     pid,
+                                                       const SpCaptureAddress  *addrs,
+                                                       guint                    n_addrs);
+gboolean            sp_capture_writer_add_fork        (SpCaptureWriter         *self,
+                                                       gint64                   time,
+                                                       gint                     cpu,
+                                                       GPid                     pid,
+                                                       GPid                     child_pid);
+gboolean            sp_capture_writer_add_exit        (SpCaptureWriter         *self,
+                                                       gint64                   time,
+                                                       gint                     cpu,
+                                                       GPid                     pid);
+gboolean            sp_capture_writer_add_timestamp   (SpCaptureWriter         *self,
+                                                       gint64                   time,
+                                                       gint                     cpu,
+                                                       GPid                     pid);
+gboolean            sp_capture_writer_flush           (SpCaptureWriter         *self);
+gboolean            sp_capture_writer_save_as         (SpCaptureWriter         *self,
+                                                       const gchar             *filename,
+                                                       GError                 **error);
+SpCaptureReader    *sp_capture_writer_create_reader   (SpCaptureWriter         *self,
+                                                       GError                 **error);
+gboolean            sp_capture_writer_splice          (SpCaptureWriter         *self,
+                                                       SpCaptureWriter         *dest,
+                                                       GError                 **error);
+gboolean            _sp_capture_writer_splice_from_fd (SpCaptureWriter         *self,
+                                                       int                      fd,
+                                                       GError                 **error) G_GNUC_INTERNAL;
+
+#ifndef SP_DISABLE_GOBJECT
+# define SP_TYPE_CAPTURE_WRITER (sp_capture_writer_get_type())
+  GType sp_capture_writer_get_type (void);
+#endif
+
+#if GLIB_CHECK_VERSION(2, 44, 0)
+  G_DEFINE_AUTOPTR_CLEANUP_FUNC (SpCaptureWriter, sp_capture_writer_unref)
+#endif
+
+G_END_DECLS
+
+#endif /* SP_CAPTURE_WRITER_H */


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