Blob Blame History Raw
From 5b03f23e01dfeb68846fcd280ba6ee9d4dd6113a Mon Sep 17 00:00:00 2001
From: "Richard W.M. Jones" <rjones@redhat.com>
Date: Tue, 3 May 2016 11:53:57 +0100
Subject: [PATCH] utils: Move tests/qemu/ boot-analysis etc tools to new utils
 top level directory.

Create a new top level directory called 'utils' and move the
following programs there:

  tests/qemu/boot-analysis -> utils/boot-analysis/
  tests/qemu/boot-benchmark -> utils/boot-benchmark/
  tests/qemu/qemu-boot -> utils/qemu-boot/
  tests/qemu/qemu-speed-test -> utils/qemu-speed-test/

Also we only build the boot-analysis program on x86-64 and aarch64,
since it requires custom porting to each architecture.

(cherry picked from commit 3b581a727c9e906f9d9b10e2f73ed853ab324fd0)
---
 .gitignore                                   |    8 +-
 Makefile.am                                  |    9 +
 configure.ac                                 |   10 +-
 docs/guestfs-hacking.pod                     |    4 +
 docs/guestfs-performance.pod                 |   30 +-
 m4/guestfs_misc.m4                           |   28 +
 po/POTFILES                                  |    7 +
 tests/qemu/Makefile.am                       |   87 +-
 tests/qemu/boot-analysis-timeline.c          |  523 -----------
 tests/qemu/boot-analysis-utils.c             |   90 --
 tests/qemu/boot-analysis-utils.h             |   36 -
 tests/qemu/boot-analysis.c                   | 1269 --------------------------
 tests/qemu/boot-analysis.h                   |  102 ---
 tests/qemu/boot-benchmark-range.pl           |  240 -----
 tests/qemu/boot-benchmark.c                  |  225 -----
 tests/qemu/qemu-boot.c                       |  362 --------
 tests/qemu/qemu-speed-test.c                 |  480 ----------
 utils/boot-analysis/Makefile.am              |   43 +
 utils/boot-analysis/boot-analysis-timeline.c |  523 +++++++++++
 utils/boot-analysis/boot-analysis-utils.c    |   90 ++
 utils/boot-analysis/boot-analysis-utils.h    |   36 +
 utils/boot-analysis/boot-analysis.c          | 1268 +++++++++++++++++++++++++
 utils/boot-analysis/boot-analysis.h          |  102 +++
 utils/boot-benchmark/Makefile.am             |   44 +
 utils/boot-benchmark/boot-benchmark-range.pl |  240 +++++
 utils/boot-benchmark/boot-benchmark.c        |  225 +++++
 utils/qemu-boot/Makefile.am                  |   39 +
 utils/qemu-boot/qemu-boot.c                  |  362 ++++++++
 utils/qemu-speed-test/Makefile.am            |   36 +
 utils/qemu-speed-test/qemu-speed-test.c      |  480 ++++++++++
 30 files changed, 3562 insertions(+), 3436 deletions(-)
 create mode 100644 m4/guestfs_misc.m4
 delete mode 100644 tests/qemu/boot-analysis-timeline.c
 delete mode 100644 tests/qemu/boot-analysis-utils.c
 delete mode 100644 tests/qemu/boot-analysis-utils.h
 delete mode 100644 tests/qemu/boot-analysis.c
 delete mode 100644 tests/qemu/boot-analysis.h
 delete mode 100755 tests/qemu/boot-benchmark-range.pl
 delete mode 100644 tests/qemu/boot-benchmark.c
 delete mode 100644 tests/qemu/qemu-boot.c
 delete mode 100644 tests/qemu/qemu-speed-test.c
 create mode 100644 utils/boot-analysis/Makefile.am
 create mode 100644 utils/boot-analysis/boot-analysis-timeline.c
 create mode 100644 utils/boot-analysis/boot-analysis-utils.c
 create mode 100644 utils/boot-analysis/boot-analysis-utils.h
 create mode 100644 utils/boot-analysis/boot-analysis.c
 create mode 100644 utils/boot-analysis/boot-analysis.h
 create mode 100644 utils/boot-benchmark/Makefile.am
 create mode 100755 utils/boot-benchmark/boot-benchmark-range.pl
 create mode 100644 utils/boot-benchmark/boot-benchmark.c
 create mode 100644 utils/qemu-boot/Makefile.am
 create mode 100644 utils/qemu-boot/qemu-boot.c
 create mode 100644 utils/qemu-speed-test/Makefile.am
 create mode 100644 utils/qemu-speed-test/qemu-speed-test.c

diff --git a/.gitignore b/.gitignore
index 46a6e65..d79dc98 100644
--- a/.gitignore
+++ b/.gitignore
@@ -509,10 +509,6 @@ Makefile.in
 /tests/mountable/test-internal-parse-mountable
 /tests/parallel/test-parallel
 /tests/protocol/test-error-messages
-/tests/qemu/boot-analysis
-/tests/qemu/boot-benchmark
-/tests/qemu/qemu-boot
-/tests/qemu/qemu-speed-test
 /tests/regressions/rhbz501893
 /tests/regressions/rhbz790721
 /tests/regressions/rhbz914931
@@ -561,6 +557,10 @@ Makefile.in
 /test-tool/stamp-libguestfs-test-tool.pod
 /tools/stamp-virt-*.pod
 /tools/virt-*.1
+/utils/boot-analysis/boot-analysis
+/utils/boot-benchmark/boot-benchmark
+/utils/qemu-boot/qemu-boot
+/utils/qemu-speed-test/qemu-speed-test
 /v2v/.depend
 /v2v/centos-6.img
 /v2v/centos-7.0.img
diff --git a/Makefile.am b/Makefile.am
index c77fc34..079aa7d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -165,6 +165,15 @@ if HAVE_FUSE
 SUBDIRS += fuse
 endif
 
+# Miscellaneous utilities.
+if HAVE_BOOT_ANALYSIS
+SUBDIRS += utils/boot-analysis
+endif
+SUBDIRS += \
+	utils/boot-benchmark \
+	utils/qemu-boot \
+	utils/qemu-speed-test
+
 # po-docs must come after tools, inspector.
 if HAVE_PO4A
 SUBDIRS += po-docs
diff --git a/configure.ac b/configure.ac
index 6cef0a7..0bff74f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -124,10 +124,8 @@ m4_include([m4/guestfs_gobject.m4])
 dnl Bash completion.
 m4_include([m4/guestfs_bash_completion.m4])
 
-dnl Replace libtool with a wrapper that clobbers dependency_libs in *.la files
-dnl http://lists.fedoraproject.org/pipermail/devel/2010-November/146343.html
-LIBTOOL='bash $(top_srcdir)/libtool-kill-dependency_libs.sh $(top_builddir)/libtool'
-AC_SUBST([LIBTOOL])
+dnl Miscellaneous configuration that doesn't fit anywhere else.
+m4_include([m4/guestfs_misc.m4])
 
 dnl Work around autoconf's lack of expanded variables.
 eval my_sysconfdir="\"[$]sysconfdir\""
@@ -283,6 +281,10 @@ AC_CONFIG_FILES([Makefile
                  tests/xfs/Makefile
                  tests/xml/Makefile
                  tools/Makefile
+                 utils/boot-analysis/Makefile
+                 utils/boot-benchmark/Makefile
+                 utils/qemu-boot/Makefile
+                 utils/qemu-speed-test/Makefile
                  v2v/Makefile
                  v2v/test-harness/Makefile
                  v2v/test-harness/META
diff --git a/docs/guestfs-hacking.pod b/docs/guestfs-hacking.pod
index 756c6f2..f2f8207 100644
--- a/docs/guestfs-hacking.pod
+++ b/docs/guestfs-hacking.pod
@@ -694,6 +694,10 @@ created by another.
 
 Command line tools written in Perl (L<virt-win-reg(1)> and many others).
 
+=item F<utils>
+
+Miscellaneous utilities, such as C<boot-benchmark>.
+
 =item F<v2v>
 
 L<virt-v2v(1)> command and documentation.
diff --git a/docs/guestfs-performance.pod b/docs/guestfs-performance.pod
index d9c76ac..7304b63 100644
--- a/docs/guestfs-performance.pod
+++ b/docs/guestfs-performance.pod
@@ -30,13 +30,13 @@ Run this command several times in a row and discard the first few
 runs, so that you are measuring a typical "hot cache" case.
 
 I<Side note for developers:> If you are compiling libguestfs from
-source, there is a program called F<tests/qemu/boot-benchmark> which
-does the same thing, but performs multiple runs and prints the mean
-and standard deviation.  To run it, do:
+source, there is a program called
+F<utils/boot-benchmark/boot-benchmark> which does the same thing, but
+performs multiple runs and prints the mean and standard deviation.  To
+run it, do:
 
  make
- make -C tests/qemu boot-benchmark
- ./run ./tests/qemu/boot-benchmark
+ ./run utils/boot-benchmark/boot-benchmark
 
 =head3 Explanation
 
@@ -442,17 +442,16 @@ not available.
 
 =head2 Boot analysis
 
-In the libguestfs source directory, in F<tests/qemu> is a program
-called C<boot-analysis>.  This program is able to produce a very
-detailed breakdown of the boot steps (eg. qemu, BIOS, kernel,
+In the libguestfs source directory, in F<utils/boot-analysis> is a
+program called C<boot-analysis>.  This program is able to produce a
+very detailed breakdown of the boot steps (eg. qemu, BIOS, kernel,
 libguestfs init script), and can measure how long it takes to perform
 each step.
 
 To run this program, do:
 
  make
- make -C tests/qemu boot-analysis
- ./run ./tests/qemu/boot-analysis
+ ./run utils/boot-analysis/boot-analysis
 
 =head2 Detailed timings using ts
 
@@ -594,14 +593,15 @@ bit.
 Sometimes performance regressions happen in other programs (eg. qemu,
 the kernel) that cause problems for libguestfs.
 
-In the libguestfs source, F<tests/qemu/boot-benchmark-range.pl> is a
-script which can be used to benchmark libguestfs across a range of git
-commits in another project to find out if any commit is causing a
-slowdown (or speedup).
+In the libguestfs source,
+F<utils/boot-benchmark/boot-benchmark-range.pl> is a script which can
+be used to benchmark libguestfs across a range of git commits in
+another project to find out if any commit is causing a slowdown (or
+speedup).
 
 To find out how to use this script, consult the manual:
 
- ./tests/qemu/boot-benchmark-range.pl --man
+ ./utils/boot-benchmark/boot-benchmark-range.pl --man
 
 =head1 SEE ALSO
 
diff --git a/m4/guestfs_misc.m4 b/m4/guestfs_misc.m4
new file mode 100644
index 0000000..e6f4b05
--- /dev/null
+++ b/m4/guestfs_misc.m4
@@ -0,0 +1,28 @@
+# libguestfs
+# Copyright (C) 2009-2016 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+dnl Miscellaneous configuration that doesn't fit anywhere else.
+
+dnl Replace libtool with a wrapper that clobbers dependency_libs in *.la files
+dnl http://lists.fedoraproject.org/pipermail/devel/2010-November/146343.html
+LIBTOOL='bash $(top_srcdir)/libtool-kill-dependency_libs.sh $(top_builddir)/libtool'
+AC_SUBST([LIBTOOL])
+
+dnl Only build boot-analysis program on x86-64 and aarch64.  It
+dnl requires custom work to port to each architecture.
+AM_CONDITIONAL([HAVE_BOOT_ANALYSIS],
+               [test "$host_cpu" = "x86_64" || test "$host_cpu" = "aarch64"])
diff --git a/po/POTFILES b/po/POTFILES
index a5f3f9e..ebee244 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -359,6 +359,13 @@ src/utils.c
 src/wait.c
 src/whole-file.c
 test-tool/test-tool.c
+utils/boot-analysis/boot-analysis-timeline.c
+utils/boot-analysis/boot-analysis-utils.c
+utils/boot-analysis/boot-analysis.c
+utils/boot-benchmark/boot-benchmark-range.pl
+utils/boot-benchmark/boot-benchmark.c
+utils/qemu-boot/qemu-boot.c
+utils/qemu-speed-test/qemu-speed-test.c
 v2v/changeuid-c.c
 v2v/domainxml-c.c
 v2v/utils-c.c
diff --git a/tests/qemu/Makefile.am b/tests/qemu/Makefile.am
index f2171cd..472788a 100644
--- a/tests/qemu/Makefile.am
+++ b/tests/qemu/Makefile.am
@@ -28,92 +28,7 @@ TESTS = \
 
 TESTS_ENVIRONMENT = $(top_builddir)/run --test
 
-EXTRA_DIST = \
-	$(TESTS) \
-	boot-benchmark-range.pl \
-	qemu-boot.c \
-	qemu-speed-test.c
-
-# qemu-boot, qemu-speed-test, boot-analysis and boot-benchmark are
-# built but not run by default as they are mainly qemu & kernel
-# diagnostic tools.
-
-check_PROGRAMS = qemu-boot qemu-speed-test boot-analysis boot-benchmark
-
-qemu_boot_SOURCES = \
-	../../df/estimate-max-threads.c \
-	../../df/estimate-max-threads.h \
-	qemu-boot.c
-qemu_boot_CPPFLAGS = \
-	-I$(top_srcdir)/gnulib/lib -I$(top_builddir)/gnulib/lib \
-	-I$(top_srcdir)/src -I$(top_builddir)/src \
-	-I$(top_srcdir)/df
-qemu_boot_CFLAGS = \
-	-pthread \
-	$(WARN_CFLAGS) $(WERROR_CFLAGS)
-qemu_boot_LDADD = \
-	$(top_builddir)/src/libutils.la \
-	$(top_builddir)/src/libguestfs.la \
-	$(LIBXML2_LIBS) \
-	$(LIBVIRT_LIBS) \
-	$(LTLIBINTL) \
-	$(top_builddir)/gnulib/lib/libgnu.la
-
-qemu_speed_test_SOURCES = \
-	qemu-speed-test.c
-qemu_speed_test_CPPFLAGS = \
-	-I$(top_srcdir)/gnulib/lib -I$(top_builddir)/gnulib/lib \
-	-I$(top_srcdir)/src -I$(top_builddir)/src \
-	-I$(top_srcdir)/df
-qemu_speed_test_CFLAGS = \
-	$(WARN_CFLAGS) $(WERROR_CFLAGS)
-qemu_speed_test_LDADD = \
-	$(top_builddir)/src/libutils.la \
-	$(top_builddir)/src/libguestfs.la \
-	$(LIBXML2_LIBS) \
-	$(LIBVIRT_LIBS) \
-	$(LTLIBINTL) \
-	$(top_builddir)/gnulib/lib/libgnu.la
-
-boot_analysis_SOURCES = \
-	boot-analysis.c \
-	boot-analysis.h \
-	boot-analysis-timeline.c \
-	boot-analysis-utils.c \
-	boot-analysis-utils.h
-boot_analysis_CPPFLAGS = \
-	-I$(top_srcdir)/gnulib/lib -I$(top_builddir)/gnulib/lib \
-	-I$(top_srcdir)/src -I$(top_builddir)/src
-boot_analysis_CFLAGS = \
-	-pthread \
-	$(WARN_CFLAGS) $(WERROR_CFLAGS) \
-	$(PCRE_CFLAGS)
-boot_analysis_LDADD = \
-	$(top_builddir)/src/libutils.la \
-	$(top_builddir)/src/libguestfs.la \
-	$(PCRE_LIBS) \
-	$(LIBXML2_LIBS) \
-	$(LIBVIRT_LIBS) \
-	$(LTLIBINTL) \
-	$(top_builddir)/gnulib/lib/libgnu.la \
-	-lm
-
-boot_benchmark_SOURCES = \
-	boot-benchmark.c \
-	boot-analysis-utils.c \
-	boot-analysis-utils.h
-boot_benchmark_CPPFLAGS = \
-	-I$(top_srcdir)/gnulib/lib -I$(top_builddir)/gnulib/lib \
-	-I$(top_srcdir)/src -I$(top_builddir)/src
-boot_benchmark_CFLAGS = \
-	$(WARN_CFLAGS) $(WERROR_CFLAGS)
-boot_benchmark_LDADD = \
-	$(top_builddir)/src/libutils.la \
-	$(top_builddir)/src/libguestfs.la \
-	$(LIBXML2_LIBS) \
-	$(LTLIBINTL) \
-	$(top_builddir)/gnulib/lib/libgnu.la \
-	-lm
+EXTRA_DIST = $(TESTS)
 
 # Don't run these tests in parallel, since they are designed to check
 # the integrity of qemu.
diff --git a/tests/qemu/boot-analysis-timeline.c b/tests/qemu/boot-analysis-timeline.c
deleted file mode 100644
index 09a78ef..0000000
--- a/tests/qemu/boot-analysis-timeline.c
+++ /dev/null
@@ -1,523 +0,0 @@
-/* libguestfs
- * Copyright (C) 2016 Red Hat Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#include <config.h>
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdint.h>
-#include <inttypes.h>
-#include <string.h>
-#include <unistd.h>
-#include <error.h>
-#include <errno.h>
-#include <assert.h>
-
-#include <pcre.h>
-
-#include "ignore-value.h"
-
-#include "guestfs.h"
-#include "guestfs-internal-frontend.h"
-
-#include "boot-analysis.h"
-
-COMPILE_REGEXP(re_initcall_calling_module,
-               "calling  ([_A-Za-z0-9]+)\\+.*\\[([_A-Za-z0-9]+)]", 0)
-COMPILE_REGEXP(re_initcall_calling,
-               "calling  ([_A-Za-z0-9]+)\\+", 0)
-
-static void construct_initcall_timeline (void);
-
-/* "supermin: internal insmod xx.ko" -> "insmod xx.ko" */
-static char *
-translate_supermin_insmod_message (const char *message)
-{
-  char *ret;
-
-  assert (STRPREFIX (message, "supermin: internal "));
-
-  ret = strdup (message + strlen ("supermin: internal "));
-  if (ret == NULL)
-    error (EXIT_FAILURE, errno, "strdup");
-  return ret;
-}
-
-/* Analyze significant events from the events array, to form a
- * timeline of activities.
- */
-void
-construct_timeline (void)
-{
-  size_t i, j, k;
-  struct pass_data *data;
-  struct activity *activity;
-
-  for (i = 0; i < NR_TEST_PASSES; ++i) {
-    data = &pass_data[i];
-
-    /* Find an activity, by matching an event with the condition
-     * `begin_cond' through to the second event `end_cond'.  Create an
-     * activity object in the timeline from the result.
-     */
-#define FIND(name, flags, begin_cond, end_cond)                         \
-    do {                                                                \
-      activity = NULL;                                                  \
-      for (j = 0; j < data->nr_events; ++j) {                           \
-        if (begin_cond) {                                               \
-          for (k = j+1; k < data->nr_events; ++k) {                     \
-            if (end_cond) {                                             \
-              if (i == 0)                                               \
-                activity = add_activity (name, flags);                  \
-              else                                                      \
-                activity = find_activity (name);                        \
-              break;                                                    \
-            }                                                           \
-          }                                                             \
-          break;                                                        \
-        }                                                               \
-      }                                                                 \
-      if (activity) {                                                   \
-        activity->start_event[i] = j;                                   \
-        activity->end_event[i] = k;                                     \
-      }                                                                 \
-      else                                                              \
-        error (EXIT_FAILURE, 0, "could not find activity '%s' in pass '%zu'", \
-               name, i);                                                \
-    } while (0)
-
-    /* Same as FIND() macro, but if no matching events are found,
-     * ignore it.
-     */
-#define FIND_OPTIONAL(name, flags, begin_cond, end_cond)                \
-    do {                                                                \
-      activity = NULL;                                                  \
-      for (j = 0; j < data->nr_events; ++j) {                           \
-        if (begin_cond) {                                               \
-          for (k = j+1; k < data->nr_events; ++k) {                     \
-            if (end_cond) {                                             \
-              if (i == 0)                                               \
-                activity = add_activity (name, flags);                  \
-              else                                                      \
-                activity = find_activity (name);                        \
-              break;                                                    \
-            }                                                           \
-          }                                                             \
-          break;                                                        \
-        }                                                               \
-      }                                                                 \
-      if (activity) {                                                   \
-        activity->start_event[i] = j;                                   \
-        activity->end_event[i] = k;                                     \
-      }                                                                 \
-    } while (0)
-
-    /* Find multiple entries, where we check for:
-     *   next_cond
-     *   next_cond
-     *   next_cond
-     *   end_cond
-     */
-#define FIND_MULTIPLE(debug_name, flags, next_cond, end_cond, translate_message) \
-    do {                                                                \
-      activity = NULL;                                                  \
-      for (j = 0; j < data->nr_events; ++j) {                           \
-        if (next_cond) {                                                \
-          CLEANUP_FREE char *message = translate_message (data->events[j].message); \
-          if (activity)                                                 \
-            activity->end_event[i] = j;                                 \
-          if (i == 0)                                                   \
-            activity = add_activity (message, flags);                   \
-          else                                                          \
-            activity = find_activity (message);                         \
-          activity->start_event[i] = j;                                 \
-        }                                                               \
-        else if (end_cond)                                              \
-          break;                                                        \
-      }                                                                 \
-      if (j < data->nr_events && activity)                              \
-        activity->end_event[i] = j;                                     \
-      else                                                              \
-        error (EXIT_FAILURE, 0, "could not find activity '%s' in pass '%zu'", \
-               debug_name, i);                                          \
-    } while (0)
-
-    /* Add one activity which is going to cover the whole process
-     * from launch to close.  The launch event is always event 0.
-     * NB: This activity must be called "run" (see below).
-     */
-    FIND ("run", LONG_ACTIVITY,
-          j == 0, data->events[k].source == GUESTFS_EVENT_CLOSE);
-
-    /* Find where we invoke supermin --build.  This should be a null
-     * operation, but it still takes time to run the external command.
-     */
-    FIND ("supermin:build", 0,
-          data->events[j].source == GUESTFS_EVENT_LIBRARY &&
-          strstr (data->events[j].message,
-                  "begin building supermin appliance"),
-          data->events[k].source == GUESTFS_EVENT_LIBRARY &&
-          strstr (data->events[k].message,
-                  "finished building supermin appliance"));
-
-    /* Find where we invoke qemu to test features. */
-    FIND_OPTIONAL ("qemu:feature-detect", 0,
-                   data->events[j].source == GUESTFS_EVENT_LIBRARY &&
-                   strstr (data->events[j].message,
-                           "begin testing qemu features"),
-                   data->events[k].source == GUESTFS_EVENT_LIBRARY &&
-                   strstr (data->events[k].message,
-                           "finished testing qemu features"));
-
-    /* Find where we run qemu. */
-    FIND_OPTIONAL ("qemu", LONG_ACTIVITY,
-                   data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-                   strstr (data->events[j].message, "-nodefconfig"),
-                   data->events[k].source == GUESTFS_EVENT_CLOSE);
-
-    /* For the libvirt backend, connecting to libvirt, getting
-     * capabilities, parsing capabilities etc.
-     */
-    FIND_OPTIONAL ("libvirt:connect", 0,
-                   data->events[j].source == GUESTFS_EVENT_LIBRARY &&
-                   strstr (data->events[j].message, "connect to libvirt"),
-                   data->events[k].source == GUESTFS_EVENT_LIBRARY &&
-                   strstr (data->events[k].message, "successfully opened libvirt handle"));
-    FIND_OPTIONAL ("libvirt:get-libvirt-capabilities", 0,
-                   data->events[j].source == GUESTFS_EVENT_LIBRARY &&
-                   strstr (data->events[j].message, "get libvirt capabilities"),
-                   data->events[k].source == GUESTFS_EVENT_LIBRARY &&
-                   strstr (data->events[k].message, "parsing capabilities XML"));
-
-    FIND_OPTIONAL ("libguestfs:parse-libvirt-capabilities", 0,
-                   data->events[j].source == GUESTFS_EVENT_LIBRARY &&
-                   strstr (data->events[j].message, "parsing capabilities XML"),
-                   data->events[k].source == GUESTFS_EVENT_LIBRARY &&
-                   strstr (data->events[k].message, "get_backend_setting"));
-
-    FIND_OPTIONAL ("libguestfs:create-libvirt-xml", 0,
-                   data->events[j].source == GUESTFS_EVENT_LIBRARY &&
-                   strstr (data->events[j].message, "create libvirt XML"),
-                   data->events[k].source == GUESTFS_EVENT_LIBRARY &&
-                   strstr (data->events[k].message, "libvirt XML:"));
-
-#if defined(__aarch64__)
-#define FIRST_KERNEL_MESSAGE "Booting Linux on physical CPU"
-#define FIRST_FIRMWARE_MESSAGE "UEFI firmware starting"
-#else
-#define SGABIOS_STRING "\033[1;256r\033[256;256H\033[6n"
-#define FIRST_KERNEL_MESSAGE "Probing EDD"
-#define FIRST_FIRMWARE_MESSAGE SGABIOS_STRING
-#endif
-
-    /* For the libvirt backend, find the overhead of libvirt. */
-    FIND_OPTIONAL ("libvirt:overhead", 0,
-                   data->events[j].source == GUESTFS_EVENT_LIBRARY &&
-                   strstr (data->events[j].message, "launch libvirt guest"),
-                   data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-                   strstr (data->events[k].message, FIRST_FIRMWARE_MESSAGE));
-
-    /* From starting qemu up to entering the BIOS is the qemu overhead. */
-    FIND_OPTIONAL ("qemu:overhead", 0,
-                   data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-                   strstr (data->events[j].message, "-nodefconfig"),
-                   data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-                   strstr (data->events[k].message, FIRST_FIRMWARE_MESSAGE));
-
-    /* From entering the BIOS to starting the kernel is the BIOS overhead. */
-    FIND_OPTIONAL ("bios:overhead", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, FIRST_FIRMWARE_MESSAGE),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, FIRST_KERNEL_MESSAGE));
-
-#if defined(__i386__) || defined(__x86_64__)
-    /* SGABIOS (option ROM). */
-    FIND_OPTIONAL ("sgabios", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, SGABIOS_STRING),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "SeaBIOS (version"));
-#endif
-
-#if defined(__i386__) || defined(__x86_64__)
-    /* SeaBIOS. */
-    FIND ("seabios", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "SeaBIOS (version"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, FIRST_KERNEL_MESSAGE));
-#endif
-
-#if defined(__i386__) || defined(__x86_64__)
-    /* SeaBIOS - only available when using debug messages. */
-    FIND_OPTIONAL ("seabios:pci-probe", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "Searching bootorder for: /pci@"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "Scan for option roms"));
-#endif
-
-    /* Find where we run the guest kernel. */
-    FIND ("kernel", LONG_ACTIVITY,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, FIRST_KERNEL_MESSAGE),
-          data->events[k].source == GUESTFS_EVENT_CLOSE);
-
-    /* Kernel startup to userspace. */
-    FIND ("kernel:overhead", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, FIRST_KERNEL_MESSAGE),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "supermin:") &&
-          strstr (data->events[k].message, "starting up"));
-
-    /* The time taken to get into start_kernel function. */
-    FIND ("kernel:entry", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, FIRST_KERNEL_MESSAGE),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "Linux version"));
-
-#if defined(__i386__) || defined(__x86_64__)
-    /* Alternatives patching instructions (XXX not very accurate we
-     * really need some debug messages inserted into the code).
-     */
-    FIND ("kernel:alternatives", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "Last level dTLB entries"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "Freeing SMP alternatives"));
-#endif
-
-    /* ftrace patching instructions. */
-    FIND ("kernel:ftrace", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "ftrace: allocating"),
-          1);
-
-    /* All initcall functions, before we enter userspace. */
-    FIND ("kernel:initcalls-before-userspace", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "calling  "),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "Freeing unused kernel memory"));
-
-    /* Find where we run supermin mini-initrd. */
-    FIND ("supermin:mini-initrd", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "supermin:") &&
-          strstr (data->events[j].message, "starting up"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "supermin: chroot"));
-
-    /* Loading kernel modules from supermin initrd. */
-    FIND_MULTIPLE
-      ("supermin insmod", 0,
-       data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-       strstr (data->events[j].message, "supermin: internal insmod"),
-       data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-       strstr (data->events[j].message, "supermin: picked"),
-       translate_supermin_insmod_message);
-
-    /* Find where we run the /init script. */
-    FIND ("/init", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "supermin: chroot"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "guestfsd --verbose"));
-
-    /* Everything from the chroot to the first echo in the /init
-     * script counts as bash overhead.
-     */
-    FIND ("bash:overhead", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "supermin: chroot"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "Starting /init script"));
-
-    /* /init: Mount special filesystems. */
-    FIND ("/init:mount-special", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "*guestfs_boot_analysis=1*"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "kmod static-nodes"));
-
-    /* /init: Run kmod static-nodes */
-    FIND ("/init:kmod-static-nodes", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "kmod static-nodes"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "systemd-tmpfiles"));
-
-    /* /init: systemd-tmpfiles. */
-    FIND ("/init:systemd-tmpfiles", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "systemd-tmpfiles"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "udev"));
-
-    /* /init: start udevd. */
-    FIND ("/init:udev-overhead", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "udevd --daemon"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "nullglob"));
-
-    /* /init: set up network. */
-    FIND ("/init:network-overhead", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "+ ip addr"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "+ test"));
-
-    /* /init: probe MD arrays. */
-    FIND ("/init:md-probe", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "+ mdadm"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "+ modprobe dm_mod"));
-
-    /* /init: probe DM/LVM. */
-    FIND ("/init:lvm-probe", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "+ modprobe dm_mod"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "+ ldmtool"));
-
-    /* /init: probe Windows dynamic disks. */
-    FIND ("/init:windows-dynamic-disks-probe", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "+ ldmtool"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "+ test"));
-
-    /* Find where we run guestfsd. */
-    FIND ("guestfsd", 0,
-          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[j].message, "guestfsd --verbose"),
-          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-          strstr (data->events[k].message, "fsync /dev/sda"));
-
-    /* Shutdown process. */
-    FIND ("shutdown", 0,
-          data->events[j].source == GUESTFS_EVENT_TRACE &&
-          STREQ (data->events[j].message, "close"),
-          data->events[k].source == GUESTFS_EVENT_CLOSE);
-  }
-
-  construct_initcall_timeline ();
-}
-
-/* Handling of initcall is so peculiar that we hide it in a separate
- * function from the rest.
- */
-static void
-construct_initcall_timeline (void)
-{
-  size_t i, j, k;
-  struct pass_data *data;
-  struct activity *activity;
-
-  for (i = 0; i < NR_TEST_PASSES; ++i) {
-    data = &pass_data[i];
-
-    /* Each kernel initcall is bracketed by:
-     *
-     * calling  ehci_hcd_init+0x0/0xc1 @ 1"
-     * initcall ehci_hcd_init+0x0/0xc1 returned 0 after 420 usecs"
-     *
-     * For initcall functions in modules:
-     *
-     * calling  virtio_mmio_init+0x0/0x1000 [virtio_mmio] @ 1"
-     * initcall virtio_mmio_init+0x0/0x1000 [virtio_mmio] returned 0 after 14 usecs"
-     *
-     * Initcall functions can be nested, and do not have unique names.
-     */
-    for (j = 0; j < data->nr_events; ++j) {
-      int vec[30], r;
-      const char *message = data->events[j].message;
-
-      if (data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
-          ((r = pcre_exec (re_initcall_calling_module, NULL,
-                           message, strlen (message),
-                           0, 0, vec, sizeof vec / sizeof vec[0])) >= 1 ||
-           (r = pcre_exec (re_initcall_calling, NULL,
-                           message, strlen (message),
-                           0, 0, vec, sizeof vec / sizeof vec[0])) >= 1)) {
-
-        CLEANUP_FREE char *fn_name = NULL, *module_name = NULL;
-        if (r >= 2) /* because pcre_exec returns 1 + number of captures */
-          fn_name = strndup (message + vec[2], vec[3]-vec[2]);
-        if (r >= 3)
-          module_name = strndup (message + vec[4], vec[5]-vec[4]);
-
-        CLEANUP_FREE char *fullname;
-        if (asprintf (&fullname, "%s.%s",
-                      module_name ? module_name : "kernel", fn_name) == -1)
-          error (EXIT_FAILURE, errno, "asprintf");
-
-        CLEANUP_FREE char *initcall_match;
-        if (asprintf (&initcall_match, "initcall %s", fn_name) == -1)
-          error (EXIT_FAILURE, errno, "asprintf");
-
-        /* Get a unique name for this activity.  Unfortunately
-         * kernel initcall function names are not unique!
-         */
-        CLEANUP_FREE char *activity_name;
-        if (asprintf (&activity_name, "initcall %s", fullname) == -1)
-          error (EXIT_FAILURE, errno, "asprintf");
-
-        if (i == 0) {
-          int n = 1;
-          while (activity_exists (activity_name)) {
-            free (activity_name);
-            if (asprintf (&activity_name, "initcall %s:%d", fullname, n) == -1)
-              error (EXIT_FAILURE, errno, "asprintf");
-            n++;
-          }
-        }
-        else {
-          int n = 1;
-          while (!activity_exists_with_no_data (activity_name, i)) {
-            free (activity_name);
-            if (asprintf (&activity_name, "initcall %s:%d", fullname, n) == -1)
-              error (EXIT_FAILURE, errno, "asprintf");
-            n++;
-          }
-        }
-
-        /* Find the matching end event.  It might be some time later,
-         * since it appears initcalls can be nested.
-         */
-        for (k = j+1; k < data->nr_events; ++k) {
-          if (data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
-              strstr (data->events[k].message, initcall_match)) {
-            if (i == 0)
-              activity = add_activity (activity_name, 0);
-            else
-              activity = find_activity (activity_name);
-            activity->start_event[i] = j;
-            activity->end_event[i] = k;
-            break;
-          }
-        }
-      }
-    }
-  }
-}
diff --git a/tests/qemu/boot-analysis-utils.c b/tests/qemu/boot-analysis-utils.c
deleted file mode 100644
index 693b6f4..0000000
--- a/tests/qemu/boot-analysis-utils.c
+++ /dev/null
@@ -1,90 +0,0 @@
-/* libguestfs
- * Copyright (C) 2016 Red Hat Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#include <config.h>
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <time.h>
-#include <error.h>
-#include <errno.h>
-
-#include "ignore-value.h"
-
-#include "guestfs.h"
-#include "guestfs-internal-frontend.h"
-
-#include "boot-analysis-utils.h"
-
-void
-get_time (struct timespec *ts)
-{
-  if (clock_gettime (CLOCK_REALTIME, ts) == -1)
-    error (EXIT_FAILURE, errno, "clock_gettime: CLOCK_REALTIME");
-}
-
-int64_t
-timespec_diff (const struct timespec *x, const struct timespec *y)
-{
-  int64_t nsec;
-
-  nsec = (y->tv_sec - x->tv_sec) * UINT64_C(1000000000);
-  nsec += y->tv_nsec - x->tv_nsec;
-  return nsec;
-}
-
-void
-test_info (guestfs_h *g, int nr_test_passes)
-{
-  const char *qemu = guestfs_get_hv (g);
-  CLEANUP_FREE char *cmd = NULL;
-  CLEANUP_FREE char *backend = NULL;
-
-  /* Related to the test program. */
-  printf ("test version: %s %s\n", PACKAGE_NAME, PACKAGE_VERSION_FULL);
-  printf (" test passes: %d\n", nr_test_passes);
-
-  /* Related to the host. */
-  printf ("host version: ");
-  fflush (stdout);
-  ignore_value (system ("uname -a"));
-  printf ("    host CPU: ");
-  fflush (stdout);
-  ignore_value (system ("perl -n -e 'if (/^model name.*: (.*)/) { print \"$1\\n\"; exit }' /proc/cpuinfo"));
-
-  /* Related to qemu. */
-  backend = guestfs_get_backend (g);
-  printf ("     backend: %-20s [to change set LIBGUESTFS_BACKEND]\n",
-          backend);
-  printf ("        qemu: %-20s [to change set $LIBGUESTFS_HV]\n", qemu);
-  printf ("qemu version: ");
-  fflush (stdout);
-  if (asprintf (&cmd, "%s -version", qemu) == -1)
-    error (EXIT_FAILURE, errno, "asprintf");
-  ignore_value (system (cmd));
-  printf ("         smp: %-20d [to change use --smp option]\n",
-          guestfs_get_smp (g));
-  printf ("     memsize: %-20d [to change use --memsize option]\n",
-          guestfs_get_memsize (g));
-
-  /* Related to the guest kernel.  Be nice to get the guest
-   * kernel version here somehow (XXX).
-   */
-  printf ("      append: %-20s [to change use --append option]\n",
-          guestfs_get_append (g) ? : "");
-}
diff --git a/tests/qemu/boot-analysis-utils.h b/tests/qemu/boot-analysis-utils.h
deleted file mode 100644
index 95e4f06..0000000
--- a/tests/qemu/boot-analysis-utils.h
+++ /dev/null
@@ -1,36 +0,0 @@
-/* libguestfs
- * Copyright (C) 2016 Red Hat Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#ifndef GUESTFS_BOOT_ANALYSIS_UTILS_H_
-#define GUESTFS_BOOT_ANALYSIS_UTILS_H_
-
-/* Get current time, returning it in *ts.  If there is a system call
- * failure, this exits.
- */
-extern void get_time (struct timespec *ts);
-
-/* Computes Y - X, returning nanoseconds. */
-extern int64_t timespec_diff (const struct timespec *x, const struct timespec *y);
-
-/* Display host machine and test parameters (to stdout).  'g' should
- * be an open libguestfs handle.  It is used for reading hv, memsize
- * etc. and is not modified.
- */
-extern void test_info (guestfs_h *g, int nr_test_passes);
-
-#endif /* GUESTFS_BOOT_ANALYSIS_UTILS_H_ */
diff --git a/tests/qemu/boot-analysis.c b/tests/qemu/boot-analysis.c
deleted file mode 100644
index 2067dfc..0000000
--- a/tests/qemu/boot-analysis.c
+++ /dev/null
@@ -1,1269 +0,0 @@
-/* libguestfs
- * Copyright (C) 2016 Red Hat Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-/* Trace and analyze the appliance boot process to find out which
- * steps are taking the most time.  It is not part of the standard
- * tests.
- *
- * This needs to be run on a quiet machine, so that other processes
- * disturb the timing as little as possible.  The program is
- * completely safe to run at any time.  It doesn't read or write any
- * external files, and it doesn't require root.
- *
- * You can run it from the build directory like this:
- *
- *   make
- *   make -C tests/qemu boot-analysis
- *   ./run tests/qemu/boot-analysis
- *
- * The way it works is roughly like this:
- *
- * We create a libguestfs handle and register callback handlers so we
- * can see appliance messages, trace events and so on.
- *
- * We then launch the handle and shut it down as quickly as possible.
- *
- * While the handle is running, events (seen by the callback handlers)
- * are written verbatim into an in-memory buffer, with timestamps.
- *
- * Afterwards we analyze the result using regular expressions to try
- * to identify a "timeline" for the handle (eg. at what time did the
- * BIOS hand control to the kernel).  This analysis is done in
- * 'boot-analysis-timeline.c'.
- *
- * The whole process is repeated across a few runs, and the final
- * timeline (including statistical analysis of the variation between
- * runs) gets printed.
- *
- * The program is very sensitive to the specific messages printed by
- * BIOS/kernel/supermin/userspace, so it won't work on non-x86, and it
- * will require periodic adjustment of the regular expressions in
- * order to keep things up to date.
- */
-
-#include <config.h>
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdint.h>
-#include <inttypes.h>
-#include <string.h>
-#include <unistd.h>
-#include <fcntl.h>
-#include <getopt.h>
-#include <limits.h>
-#include <time.h>
-#include <errno.h>
-#include <error.h>
-#include <ctype.h>
-#include <assert.h>
-#include <sys/types.h>
-#include <sys/wait.h>
-#include <math.h>
-#include <pthread.h>
-
-#include "guestfs.h"
-#include "guestfs-internal-frontend.h"
-
-#include "boot-analysis.h"
-#include "boot-analysis-utils.h"
-
-/* Activities taking longer than this % of the total time, except
- * those flagged as LONG_ACTIVITY, are highlighted in red.
- */
-#define WARNING_THRESHOLD 1.0
-
-static const char *append = NULL;
-static int force_colour = 0;
-static int memsize = 0;
-static int smp = 1;
-static int verbose = 0;
-
-static int libvirt_pipe[2] = { -1, -1 };
-static ssize_t libvirt_pass = -1;
-
-/* Because there is a separate thread which collects libvirt log data,
- * we must protect the pass_data struct with a mutex.  This only
- * applies during the data collection passes.
- */
-static pthread_mutex_t pass_data_lock = PTHREAD_MUTEX_INITIALIZER;
-struct pass_data pass_data[NR_TEST_PASSES];
-
-size_t nr_activities;
-struct activity *activities;
-
-static void run_test (void);
-static struct event *add_event (struct pass_data *, uint64_t source);
-static guestfs_h *create_handle (void);
-static void set_up_event_handlers (guestfs_h *g, size_t pass);
-static void libvirt_log_hack (int argc, char **argv);
-static void start_libvirt_thread (size_t pass);
-static void stop_libvirt_thread (void);
-static void add_drive (guestfs_h *g);
-static void check_pass_data (void);
-static void dump_pass_data (void);
-static void analyze_timeline (void);
-static void dump_timeline (void);
-static void print_analysis (void);
-static void print_longest_to_shortest (void);
-static void free_pass_data (void);
-static void free_final_timeline (void);
-static void ansi_green (void);
-static void ansi_red (void);
-static void ansi_blue (void);
-static void ansi_magenta (void);
-static void ansi_restore (void);
-
-static void
-usage (int exitcode)
-{
-  guestfs_h *g;
-  int default_memsize = -1;
-
-  g = guestfs_create ();
-  if (g) {
-    default_memsize = guestfs_get_memsize (g);
-    guestfs_close (g);
-  }
-
-  fprintf (stderr,
-           "boot-analysis: Trace and analyze the appliance boot process.\n"
-           "Usage:\n"
-           "  boot-analysis [--options]\n"
-           "Options:\n"
-           "  --help         Display this usage text and exit.\n"
-           "  --append OPTS  Append OPTS to kernel command line.\n"
-           "  --colour       Output colours, even if not a terminal.\n"
-           "  -m MB\n"
-           "  --memsize MB   Set memory size in MB (default: %d).\n"
-           "  --smp N        Enable N virtual CPUs (default: 1).\n"
-           "  -v|--verbose   Verbose output, useful for debugging.\n",
-           default_memsize);
-  exit (exitcode);
-}
-
-int
-main (int argc, char *argv[])
-{
-  enum { HELP_OPTION = CHAR_MAX + 1 };
-  static const char *options = "m:v";
-  static const struct option long_options[] = {
-    { "help", 0, 0, HELP_OPTION },
-    { "append", 1, 0, 0 },
-    { "color", 0, 0, 0 },
-    { "colour", 0, 0, 0 },
-    { "memsize", 1, 0, 'm' },
-    { "libvirt-pipe-0", 1, 0, 0 }, /* see libvirt_log_hack */
-    { "libvirt-pipe-1", 1, 0, 0 },
-    { "smp", 1, 0, 0 },
-    { "verbose", 0, 0, 'v' },
-    { 0, 0, 0, 0 }
-  };
-  int c, option_index;
-
-  for (;;) {
-    c = getopt_long (argc, argv, options, long_options, &option_index);
-    if (c == -1) break;
-
-    switch (c) {
-    case 0:                     /* Options which are long only. */
-      if (STREQ (long_options[option_index].name, "append")) {
-        append = optarg;
-        break;
-      }
-      else if (STREQ (long_options[option_index].name, "color") ||
-               STREQ (long_options[option_index].name, "colour")) {
-        force_colour = 1;
-        break;
-      }
-      else if (STREQ (long_options[option_index].name, "libvirt-pipe-0")) {
-        if (sscanf (optarg, "%d", &libvirt_pipe[0]) != 1)
-          error (EXIT_FAILURE, 0,
-                 "could not parse libvirt-pipe-0 parameter: %s", optarg);
-        break;
-      }
-      else if (STREQ (long_options[option_index].name, "libvirt-pipe-1")) {
-        if (sscanf (optarg, "%d", &libvirt_pipe[1]) != 1)
-          error (EXIT_FAILURE, 0,
-                 "could not parse libvirt-pipe-1 parameter: %s", optarg);
-        break;
-      }
-      else if (STREQ (long_options[option_index].name, "smp")) {
-        if (sscanf (optarg, "%d", &smp) != 1)
-          error (EXIT_FAILURE, 0,
-                 "could not parse smp parameter: %s", optarg);
-        break;
-      }
-      fprintf (stderr, "%s: unknown long option: %s (%d)\n",
-               guestfs_int_program_name, long_options[option_index].name, option_index);
-      exit (EXIT_FAILURE);
-
-    case 'm':
-      if (sscanf (optarg, "%d", &memsize) != 1) {
-        fprintf (stderr, "%s: could not parse memsize parameter: %s\n",
-                 guestfs_int_program_name, optarg);
-        exit (EXIT_FAILURE);
-      }
-      break;
-
-    case 'v':
-      verbose = 1;
-      break;
-
-    case HELP_OPTION:
-      usage (EXIT_SUCCESS);
-
-    default:
-      usage (EXIT_FAILURE);
-    }
-  }
-
-  libvirt_log_hack (argc, argv);
-
-  if (STRNEQ (host_cpu, "x86_64") && STRNEQ (host_cpu, "aarch64"))
-    fprintf (stderr, "WARNING: host_cpu != x86_64|aarch64: This program may not work or give bogus results.\n");
-
-  run_test ();
-}
-
-static void
-run_test (void)
-{
-  guestfs_h *g;
-  size_t i;
-
-  printf ("Warming up the libguestfs cache ...\n");
-  for (i = 0; i < NR_WARMUP_PASSES; ++i) {
-    g = create_handle ();
-    add_drive (g);
-    if (guestfs_launch (g) == -1)
-      exit (EXIT_FAILURE);
-    guestfs_close (g);
-  }
-
-  printf ("Running the tests in %d passes ...\n", NR_TEST_PASSES);
-  for (i = 0; i < NR_TEST_PASSES; ++i) {
-    g = create_handle ();
-    set_up_event_handlers (g, i);
-    start_libvirt_thread (i);
-    add_drive (g);
-    if (guestfs_launch (g) == -1)
-      exit (EXIT_FAILURE);
-    guestfs_close (g);
-    stop_libvirt_thread ();
-
-    printf ("    pass %zu: %zu events collected in %" PRIi64 " ns\n",
-            i+1, pass_data[i].nr_events, pass_data[i].elapsed_ns);
-  }
-
-  if (verbose)
-    dump_pass_data ();
-
-  printf ("Analyzing the results ...\n");
-  check_pass_data ();
-  construct_timeline ();
-  analyze_timeline ();
-
-  if (verbose)
-    dump_timeline ();
-
-  printf ("\n");
-  g = create_handle ();
-  test_info (g, NR_TEST_PASSES);
-  guestfs_close (g);
-  printf ("\n");
-  print_analysis ();
-  printf ("\n");
-  printf ("Longest activities:\n");
-  printf ("\n");
-  print_longest_to_shortest ();
-
-  free_pass_data ();
-  free_final_timeline ();
-}
-
-static struct event *
-add_event_unlocked (struct pass_data *data, uint64_t source)
-{
-  struct event *ret;
-
-  data->nr_events++;
-  data->events = realloc (data->events,
-                          sizeof (struct event) * data->nr_events);
-  if (data->events == NULL)
-    error (EXIT_FAILURE, errno, "realloc");
-  ret = &data->events[data->nr_events-1];
-  get_time (&ret->t);
-  ret->source = source;
-  ret->message = NULL;
-  return ret;
-}
-
-static struct event *
-add_event (struct pass_data *data, uint64_t source)
-{
-  struct event *ret;
-
-  pthread_mutex_lock (&pass_data_lock);
-  ret = add_event_unlocked (data, source);
-  pthread_mutex_unlock (&pass_data_lock);
-  return ret;
-}
-
-/* Common function to create the handle and set various defaults. */
-static guestfs_h *
-create_handle (void)
-{
-  guestfs_h *g;
-  CLEANUP_FREE char *full_append = NULL;
-
-  g = guestfs_create ();
-  if (!g) error (EXIT_FAILURE, errno, "guestfs_create");
-
-  if (memsize != 0)
-    if (guestfs_set_memsize (g, memsize) == -1)
-      exit (EXIT_FAILURE);
-
-  if (smp >= 2)
-    if (guestfs_set_smp (g, smp) == -1)
-      exit (EXIT_FAILURE);
-
-  /* This changes some details in appliance/init and enables a
-   * detailed trace of calls to initcall functions in the kernel.
-   */
-  if (asprintf (&full_append,
-                "guestfs_boot_analysis=1 "
-                "ignore_loglevel initcall_debug "
-                "%s",
-                append != NULL ? append : "") == -1)
-    error (EXIT_FAILURE, errno, "asprintf");
-
-  if (guestfs_set_append (g, full_append) == -1)
-    exit (EXIT_FAILURE);
-
-  return g;
-}
-
-/* Common function to add the /dev/null drive. */
-static void
-add_drive (guestfs_h *g)
-{
-  if (guestfs_add_drive_opts (g, "/dev/null",
-                              GUESTFS_ADD_DRIVE_OPTS_FORMAT, "raw",
-                              GUESTFS_ADD_DRIVE_OPTS_READONLY, 1,
-                              -1) == -1)
-    exit (EXIT_FAILURE);
-}
-
-/* Called when the handle is closed.  Perform any cleanups required in
- * the pass_data here.
- */
-static void
-close_callback (guestfs_h *g, void *datavp, uint64_t source,
-                int eh, int flags,
-                const char *buf, size_t buf_len,
-                const uint64_t *array, size_t array_len)
-{
-  struct pass_data *data = datavp;
-  struct event *event;
-
-  if (!data->seen_launch)
-    return;
-
-  event = add_event (data, source);
-  event->message = strdup ("close callback");
-  if (event->message == NULL)
-    error (EXIT_FAILURE, errno, "strdup");
-
-  get_time (&data->end_t);
-  data->elapsed_ns = timespec_diff (&data->start_t, &data->end_t);
-}
-
-/* Called when the qemu subprocess exits.
- * XXX This is never called - why?
- */
-static void
-subprocess_quit_callback (guestfs_h *g, void *datavp, uint64_t source,
-                          int eh, int flags,
-                          const char *buf, size_t buf_len,
-                          const uint64_t *array, size_t array_len)
-{
-  struct pass_data *data = datavp;
-  struct event *event;
-
-  if (!data->seen_launch)
-    return;
-
-  event = add_event (data, source);
-  event->message = strdup ("subprocess quit callback");
-  if (event->message == NULL)
-    error (EXIT_FAILURE, errno, "strdup");
-}
-
-/* Called when the launch operation is complete (the library and the
- * guestfs daemon and talking to each other).
- */
-static void
-launch_done_callback (guestfs_h *g, void *datavp, uint64_t source,
-                      int eh, int flags,
-                      const char *buf, size_t buf_len,
-                      const uint64_t *array, size_t array_len)
-{
-  struct pass_data *data = datavp;
-  struct event *event;
-
-  if (!data->seen_launch)
-    return;
-
-  event = add_event (data, source);
-  event->message = strdup ("launch done callback");
-  if (event->message == NULL)
-    error (EXIT_FAILURE, errno, "strdup");
-}
-
-/* Trim \r (multiple) from the end of a string. */
-static void
-trim_r (char *message)
-{
-  size_t len = strlen (message);
-
-  while (len > 0 && message[len-1] == '\r') {
-    message[len-1] = '\0';
-    len--;
-  }
-}
-
-/* Called when we get (possibly part of) a log message (or more than
- * one log message) from the appliance (which may include qemu, the
- * BIOS, kernel, etc).
- */
-static void
-appliance_callback (guestfs_h *g, void *datavp, uint64_t source,
-                    int eh, int flags,
-                    const char *buf, size_t buf_len,
-                    const uint64_t *array, size_t array_len)
-{
-  struct pass_data *data = datavp;
-  struct event *event;
-  size_t i, len, slen;
-
-  if (!data->seen_launch)
-    return;
-
-  /* If the previous log message was incomplete, but time has moved on
-   * a lot, record a new log message anyway, so it gets a new
-   * timestamp.
-   */
-  if (data->incomplete_log_message >= 0) {
-    struct timespec ts;
-    get_time (&ts);
-    if (timespec_diff (&data->events[data->incomplete_log_message].t,
-                       &ts) >= 10000000 /* 10ms */)
-      data->incomplete_log_message = -1;
-  }
-
-  /* If the previous log message was incomplete then we may need to
-   * append part of the current log message to a previous one.
-   */
-  if (data->incomplete_log_message >= 0) {
-    len = buf_len;
-    for (i = 0; i < buf_len; ++i) {
-      if (buf[i] == '\n') {
-        len = i;
-        break;
-      }
-    }
-
-    event = &data->events[data->incomplete_log_message];
-    slen = strlen (event->message);
-    event->message = realloc (event->message, slen + len + 1);
-    if (event->message == NULL)
-      error (EXIT_FAILURE, errno, "realloc");
-    memcpy (event->message + slen, buf, len);
-    event->message[slen + len] = '\0';
-    trim_r (event->message);
-
-    /* Skip what we just added to the previous incomplete message. */
-    buf += len;
-    buf_len -= len;
-
-    if (buf_len == 0)          /* still not complete, more to come! */
-      return;
-
-    /* Skip the \n in the buffer. */
-    buf++;
-    buf_len--;
-    data->incomplete_log_message = -1;
-  }
-
-  /* Add the event, or perhaps multiple events if the message
-   * contains \n characters.
-   */
-  while (buf_len > 0) {
-    len = buf_len;
-    for (i = 0; i < buf_len; ++i) {
-      if (buf[i] == '\n') {
-        len = i;
-        break;
-      }
-    }
-
-    event = add_event (data, source);
-    event->message = strndup (buf, len);
-    if (event->message == NULL)
-      error (EXIT_FAILURE, errno, "strndup");
-    trim_r (event->message);
-
-    /* Skip what we just added to the event. */
-    buf += len;
-    buf_len -= len;
-
-    if (buf_len == 0) {
-      /* Event is incomplete (doesn't end with \n).  We'll finish it
-       * in the next callback.
-       */
-      data->incomplete_log_message = event - data->events;
-      return;
-    }
-
-    /* Skip the \n in the buffer. */
-    buf++;
-    buf_len--;
-  }
-}
-
-/* Called when we get a debug message from the library side.  These
- * are always delivered as complete messages.
- */
-static void
-library_callback (guestfs_h *g, void *datavp, uint64_t source,
-                  int eh, int flags,
-                  const char *buf, size_t buf_len,
-                  const uint64_t *array, size_t array_len)
-{
-  struct pass_data *data = datavp;
-  struct event *event;
-
-  if (!data->seen_launch)
-    return;
-
-  event = add_event (data, source);
-  event->message = strndup (buf, buf_len);
-  if (event->message == NULL)
-    error (EXIT_FAILURE, errno, "strndup");
-}
-
-/* Called when we get a call trace message (a libguestfs API function
- * has been called or is returning).  These are always delivered as
- * complete messages.
- */
-static void
-trace_callback (guestfs_h *g, void *datavp, uint64_t source,
-                int eh, int flags,
-                const char *buf, size_t buf_len,
-                const uint64_t *array, size_t array_len)
-{
-  struct pass_data *data = datavp;
-  struct event *event;
-  char *message;
-
-  message = strndup (buf, buf_len);
-  if (message == NULL)
-    error (EXIT_FAILURE, errno, "strndup");
-
-  if (STREQ (message, "launch"))
-    data->seen_launch = 1;
-
-  if (!data->seen_launch) {
-    free (message);
-    return;
-  }
-
-  event = add_event (data, source);
-  event->message = message;
-}
-
-/* Common function to set up event callbacks and record data in memory
- * for a particular pass (0 <= pass < NR_TEST_PASSES).
- */
-static void
-set_up_event_handlers (guestfs_h *g, size_t pass)
-{
-  struct pass_data *data;
-
-  assert (/* 0 <= pass && */ pass < NR_TEST_PASSES);
-
-  data = &pass_data[pass];
-  data->pass = pass;
-  data->nr_events = 0;
-  data->events = NULL;
-  get_time (&data->start_t);
-  data->incomplete_log_message = -1;
-  data->seen_launch = 0;
-
-  guestfs_set_event_callback (g, close_callback,
-                              GUESTFS_EVENT_CLOSE, 0, data);
-  guestfs_set_event_callback (g, subprocess_quit_callback,
-                              GUESTFS_EVENT_SUBPROCESS_QUIT, 0, data);
-  guestfs_set_event_callback (g, launch_done_callback,
-                              GUESTFS_EVENT_LAUNCH_DONE, 0, data);
-  guestfs_set_event_callback (g, appliance_callback,
-                              GUESTFS_EVENT_APPLIANCE, 0, data);
-  guestfs_set_event_callback (g, library_callback,
-                              GUESTFS_EVENT_LIBRARY, 0, data);
-  guestfs_set_event_callback (g, trace_callback,
-                              GUESTFS_EVENT_TRACE, 0, data);
-
-  guestfs_set_verbose (g, 1);
-  guestfs_set_trace (g, 1);
-}
-
-/* libvirt debugging sucks in a number of concrete ways:
- *
- * - you can't get a synchronous callback from a log message
- * - you can't enable logging per handle (only globally
- *   by setting environment variables)
- * - you can't debug the daemon easily
- * - it's very complex
- * - it's very complex but not in ways that are practical or useful
- *
- * To get log messages at all, we need to create a pipe connected to a
- * second thread, and when libvirt prints something to the pipe we log
- * that.
- *
- * However that's not sufficient.  Because logging is only enabled
- * when libvirt examines environment variables at the start of the
- * program, we need to create the pipe and then fork+exec a new
- * instance of the whole program with the pipe and environment
- * variables set up.
- */
-static int is_libvirt_backend (guestfs_h *g);
-static void *libvirt_log_thread (void *datavp);
-
-static void
-libvirt_log_hack (int argc, char **argv)
-{
-  guestfs_h *g;
-
-  g = guestfs_create ();
-  if (!is_libvirt_backend (g)) {
-    guestfs_close (g);
-    return;
-  }
-  guestfs_close (g);
-
-  /* Have we set up the pipes and environment and forked yet?  If not,
-   * do that first.
-   */
-  if (libvirt_pipe[0] == -1 || libvirt_pipe[1] == -1) {
-    char log_outputs[64];
-    char **new_argv;
-    char param1[64], param2[64];
-    size_t i;
-    pid_t pid;
-    int status;
-
-    /* Create the pipe.  NB: do NOT use O_CLOEXEC since we want to pass
-     * this pipe into a child process.
-     */
-    if (pipe (libvirt_pipe) == -1)
-      error (EXIT_FAILURE, 0, "pipe2");
-
-    /* Create the environment variables to enable logging in libvirt. */
-    setenv ("LIBVIRT_DEBUG", "1", 1);
-    //setenv ("LIBVIRT_LOG_FILTERS",
-    //        "1:qemu 1:securit 3:file 3:event 3:object 1:util", 1);
-    snprintf (log_outputs, sizeof log_outputs,
-              "1:file:/dev/fd/%d", libvirt_pipe[1]);
-    setenv ("LIBVIRT_LOG_OUTPUTS", log_outputs, 1);
-
-    /* Run self again. */
-    new_argv = malloc ((argc+3) * sizeof (char *));
-    if (new_argv == NULL)
-      error (EXIT_FAILURE, errno, "malloc");
-
-    for (i = 0; i < (size_t) argc; ++i)
-      new_argv[i] = argv[i];
-
-    snprintf (param1, sizeof param1, "--libvirt-pipe-0=%d", libvirt_pipe[0]);
-    new_argv[argc] = param1;
-    snprintf (param2, sizeof param2, "--libvirt-pipe-1=%d", libvirt_pipe[1]);
-    new_argv[argc+1] = param2;
-    new_argv[argc+2] = NULL;
-
-    pid = fork ();
-    if (pid == -1)
-      error (EXIT_FAILURE, errno, "fork");
-    if (pid == 0) {             /* Child process. */
-      execvp (argv[0], new_argv);
-      perror ("execvp");
-      _exit (EXIT_FAILURE);
-    }
-
-    if (waitpid (pid, &status, 0) == -1)
-      error (EXIT_FAILURE, errno, "waitpid");
-    if (WIFEXITED (status))
-      exit (WEXITSTATUS (status));
-    error (EXIT_FAILURE, 0, "unexpected exit status from process: %d", status);
-  }
-
-  /* If we reach this else clause, then we have forked.  Now we must
-   * create a thread to read events from the pipe.  This must be
-   * constantly reading from the pipe, otherwise we will deadlock.
-   * During the warm-up phase we end up throwing away messages.
-   */
-  else {
-    pthread_t thread;
-    pthread_attr_t attr;
-    int r;
-
-    r = pthread_attr_init (&attr);
-    if (r != 0)
-      error (EXIT_FAILURE, r, "pthread_attr_init");
-    r = pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
-    if (r != 0)
-      error (EXIT_FAILURE, r, "pthread_attr_setdetachstate");
-    r = pthread_create (&thread, &attr, libvirt_log_thread, NULL);
-    if (r != 0)
-      error (EXIT_FAILURE, r, "pthread_create");
-    pthread_attr_destroy (&attr);
-  }
-}
-
-static void
-start_libvirt_thread (size_t pass)
-{
-  /* In the non-libvirt case, this variable is ignored. */
-  pthread_mutex_lock (&pass_data_lock);
-  libvirt_pass = pass;
-  pthread_mutex_unlock (&pass_data_lock);
-}
-
-static void
-stop_libvirt_thread (void)
-{
-  /* In the non-libvirt case, this variable is ignored. */
-  pthread_mutex_lock (&pass_data_lock);
-  libvirt_pass = -1;
-  pthread_mutex_unlock (&pass_data_lock);
-}
-
-/* The separate "libvirt thread".  It loops reading debug messages
- * printed by libvirt and adds them to the pass_data.
- */
-static void *
-libvirt_log_thread (void *arg)
-{
-  struct event *event;
-  CLEANUP_FREE char *buf = NULL;
-  ssize_t r;
-
-  buf = malloc (BUFSIZ);
-  if (buf == NULL)
-    error (EXIT_FAILURE, errno, "malloc");
-
-  while ((r = read (libvirt_pipe[0], buf, BUFSIZ)) > 0) {
-    pthread_mutex_lock (&pass_data_lock);
-    if (libvirt_pass == -1) goto discard;
-    event =
-      add_event_unlocked (&pass_data[libvirt_pass], SOURCE_LIBVIRT);
-    event->message = strndup (buf, r);
-    if (event->message == NULL)
-      error (EXIT_FAILURE, errno, "strndup");
-  discard:
-    pthread_mutex_unlock (&pass_data_lock);
-  }
-
-  if (r == -1)
-    error (EXIT_FAILURE, errno, "libvirt_log_thread: read");
-
-  /* It's possible for the pipe to be closed (r == 0) if thread
-   * cancellation is delayed after the main thread exits, so just
-   * ignore that case and exit.
-   */
-  pthread_exit (NULL);
-}
-
-static int
-is_libvirt_backend (guestfs_h *g)
-{
-  CLEANUP_FREE char *backend = guestfs_get_backend (g);
-
-  return backend &&
-    (STREQ (backend, "libvirt") || STRPREFIX (backend, "libvirt:"));
-}
-
-/* Sanity check the collected events. */
-static void
-check_pass_data (void)
-{
-  size_t i, j, len;
-  int64_t ns;
-  const char *message;
-
-  for (i = 0; i < NR_TEST_PASSES; ++i) {
-    assert (pass_data[i].pass == i);
-    assert (pass_data[i].elapsed_ns > 1000);
-    assert (pass_data[i].nr_events > 0);
-    assert (pass_data[i].events != NULL);
-
-    for (j = 0; j < pass_data[i].nr_events; ++j) {
-      assert (pass_data[i].events[j].t.tv_sec > 0);
-      if (j > 0) {
-        ns = timespec_diff (&pass_data[i].events[j-1].t,
-                            &pass_data[i].events[j].t);
-        assert (ns >= 0);
-      }
-      assert (pass_data[i].events[j].source != 0);
-      message = pass_data[i].events[j].message;
-      assert (message != NULL);
-      assert (pass_data[i].events[j].source != GUESTFS_EVENT_APPLIANCE ||
-              strchr (message, '\n') == NULL);
-      len = strlen (message);
-      assert (len == 0 || message[len-1] != '\r');
-    }
-  }
-}
-
-static void
-print_escaped_string (const char *message)
-{
-  while (*message) {
-    if (isprint (*message))
-      putchar (*message);
-    else
-      printf ("\\x%02x", (unsigned int) *message);
-    message++;
-  }
-}
-
-/* Dump the events to stdout, if verbose is set. */
-static void
-dump_pass_data (void)
-{
-  size_t i, j;
-
-  for (i = 0; i < NR_TEST_PASSES; ++i) {
-    printf ("pass %zu\n", pass_data[i].pass);
-    printf ("    number of events collected %zu\n", pass_data[i].nr_events);
-    printf ("    elapsed time %" PRIi64 " ns\n", pass_data[i].elapsed_ns);
-    for (j = 0; j < pass_data[i].nr_events; ++j) {
-      int64_t ns, diff_ns;
-      CLEANUP_FREE char *source_str = NULL;
-
-      ns = timespec_diff (&pass_data[i].start_t, &pass_data[i].events[j].t);
-      source_str = source_to_string (pass_data[i].events[j].source);
-      printf ("    %.1fms ", ns / 1000000.0);
-      if (j > 0) {
-	diff_ns = timespec_diff (&pass_data[i].events[j-1].t,
-				 &pass_data[i].events[j].t);
-	printf ("(+%.1f) ", diff_ns / 1000000.0);
-      }
-      printf ("[%s] \"", source_str);
-      print_escaped_string (pass_data[i].events[j].message);
-      printf ("\"\n");
-    }
-  }
-}
-
-/* Convert source to a printable string.  The caller must free the
- * returned string.
- */
-char *
-source_to_string (uint64_t source)
-{
-  char *ret;
-
-  if (source == SOURCE_LIBVIRT) {
-    ret = strdup ("libvirt");
-    if (ret == NULL)
-      error (EXIT_FAILURE, errno, "strdup");
-  }
-  else
-    ret = guestfs_event_to_string (source);
-
-  return ret;                   /* caller frees */
-}
-
-int
-activity_exists (const char *name)
-{
-  size_t i;
-
-  for (i = 0; i < nr_activities; ++i)
-    if (STREQ (activities[i].name, name))
-      return 1;
-  return 0;
-}
-
-/* Add an activity to the global list. */
-struct activity *
-add_activity (const char *name, int flags)
-{
-  struct activity *ret;
-  size_t i;
-
-  /* You shouldn't have two activities with the same name. */
-  assert (!activity_exists (name));
-
-  nr_activities++;
-  activities = realloc (activities, sizeof (struct activity) * nr_activities);
-  if (activities == NULL)
-    error (EXIT_FAILURE, errno, "realloc");
-  ret = &activities[nr_activities-1];
-  ret->name = strdup (name);
-  if (ret->name == NULL)
-    error (EXIT_FAILURE, errno, "strdup");
-  ret->flags = flags;
-
-  for (i = 0; i < NR_TEST_PASSES; ++i)
-    ret->start_event[i] = ret->end_event[i] = 0;
-
-  return ret;
-}
-
-struct activity *
-find_activity (const char *name)
-{
-  size_t i;
-
-  for (i = 0; i < nr_activities; ++i)
-    if (STREQ (activities[i].name, name))
-      return &activities[i];
-  error (EXIT_FAILURE, 0,
-         "internal error: could not find activity '%s'", name);
-  /*NOTREACHED*/
-  abort ();
-}
-
-int
-activity_exists_with_no_data (const char *name, size_t pass)
-{
-  size_t i;
-
-  for (i = 0; i < nr_activities; ++i)
-    if (STREQ (activities[i].name, name) &&
-        activities[i].start_event[pass] == 0 &&
-        activities[i].end_event[pass] == 0)
-      return 1;
-  return 0;
-}
-
-static int
-compare_activities_by_t (const void *av, const void *bv)
-{
-  const struct activity *a = av;
-  const struct activity *b = bv;
-
-  return a->t - b->t;
-}
-
-/* Go through the activities, computing the start and elapsed time. */
-static void
-analyze_timeline (void)
-{
-  struct activity *activity;
-  size_t i, j;
-  int64_t delta_ns;
-
-  for (j = 0; j < nr_activities; ++j) {
-    activity = &activities[j];
-
-    activity->t = 0;
-    activity->mean = 0;
-    for (i = 0; i < NR_TEST_PASSES; ++i) {
-      delta_ns =
-        timespec_diff (&pass_data[i].events[0].t,
-                       &pass_data[i].events[activity->start_event[i]].t);
-      activity->t += delta_ns;
-
-      delta_ns =
-        timespec_diff (&pass_data[i].events[activity->start_event[i]].t,
-                       &pass_data[i].events[activity->end_event[i]].t);
-      activity->mean += delta_ns;
-    }
-
-    /* Divide through to get real start time and mean of each activity. */
-    activity->t /= NR_TEST_PASSES;
-    activity->mean /= NR_TEST_PASSES;
-
-    /* Calculate the end time of this activity.  It's convenient when
-     * drawing the timeline for one activity to finish just before the
-     * next activity starts, rather than having them end and start at
-     * the same time, hence ``- 1'' here.
-     */
-    activity->end_t = activity->t + activity->mean - 1;
-
-    /* The above only calculated mean.  Now we are able to
-     * calculate from the mean the variance and the standard
-     * deviation.
-     */
-    activity->variance = 0;
-    for (i = 0; i < NR_TEST_PASSES; ++i) {
-      delta_ns =
-        timespec_diff (&pass_data[i].events[activity->start_event[i]].t,
-                       &pass_data[i].events[activity->end_event[i]].t);
-      activity->variance += pow (delta_ns - activity->mean, 2);
-    }
-    activity->variance /= NR_TEST_PASSES;
-
-    activity->sd = sqrt (activity->variance);
-  }
-
-  /* Get the total mean elapsed time from the special "run" activity. */
-  activity = find_activity ("run");
-  for (j = 0; j < nr_activities; ++j) {
-    activities[j].percent = 100.0 * activities[j].mean / activity->mean;
-
-    activities[j].warning =
-      !(activities[j].flags & LONG_ACTIVITY) &&
-      activities[j].percent >= WARNING_THRESHOLD;
-  }
-
-  /* Sort the activities by start time. */
-  qsort (activities, nr_activities, sizeof (struct activity),
-         compare_activities_by_t);
-}
-
-/* Dump the timeline to stdout, if verbose is set. */
-static void
-dump_timeline (void)
-{
-  size_t i;
-
-  for (i = 0; i < nr_activities; ++i) {
-    printf ("activity %zu:\n", i);
-    printf ("    name = %s\n", activities[i].name);
-    printf ("    start - end = %.1f - %.1f\n",
-            activities[i].t, activities[i].end_t);
-    printf ("    mean elapsed = %.1f\n", activities[i].mean);
-    printf ("    variance = %.1f\n", activities[i].variance);
-    printf ("    s.d = %.1f\n", activities[i].sd);
-    printf ("    percent = %.1f\n", activities[i].percent);
-  }
-}
-
-static void
-print_activity (struct activity *activity)
-{
-  if (activity->warning) ansi_red (); else ansi_green ();
-  print_escaped_string (activity->name);
-  ansi_restore ();
-  printf (" %.1fms ±%.1fms ",
-          activity->mean / 1000000, activity->sd / 1000000);
-  if (activity->warning) ansi_red (); else ansi_green ();
-  printf ("(%.1f%%) ", activity->percent);
-  ansi_restore ();
-}
-
-static void
-print_analysis (void)
-{
-  double t = -1;                /* Current time. */
-  /* Which columns contain activities that we are displaying now?
-   * -1 == unused column, else index of an activity
-   */
-  CLEANUP_FREE ssize_t *columns = NULL;
-  const size_t nr_columns = nr_activities;
-  size_t last_free_column = 0;
-
-  size_t i, j;
-  double last_t, smallest_next_t;
-  const double MAX_T = 1e20;
-
-  columns = malloc (nr_columns * sizeof (ssize_t));
-  if (columns == NULL) error (EXIT_FAILURE, errno, "malloc");
-  for (j = 0; j < nr_columns; ++j)
-    columns[j] = -1;
-
-  for (;;) {
-    /* Find the next significant time to display, which is a time when
-     * some activity started or ended.
-     */
-    smallest_next_t = MAX_T;
-    for (i = 0; i < nr_activities; ++i) {
-      if (t < activities[i].t && activities[i].t < smallest_next_t)
-        smallest_next_t = activities[i].t;
-      else if (t < activities[i].end_t && activities[i].end_t < smallest_next_t)
-        smallest_next_t = activities[i].end_t;
-    }
-    if (smallest_next_t == MAX_T)
-      break;                    /* Finished. */
-
-    last_t = t;
-    t = smallest_next_t;
-
-    /* Draw a spacer line, but only if last_t -> t is a large jump. */
-    if (t - last_t >= 1000000 /* ns */) {
-      printf ("          ");
-      ansi_magenta ();
-      for (j = 0; j < last_free_column; ++j) {
-        if (columns[j] >= 0 &&
-            activities[columns[j]].end_t != last_t /* !▼ */)
-          printf ("│ ");
-        else
-          printf ("  ");
-      }
-      ansi_restore ();
-      printf ("\n");
-    }
-
-    /* If there are any activities that ended before this time, drop
-     * them from the columns list.
-     */
-    for (i = 0; i < nr_activities; ++i) {
-      if (activities[i].end_t < t) {
-        for (j = 0; j < nr_columns; ++j)
-          if (columns[j] == (ssize_t) i) {
-            columns[j] = -1;
-            break;
-          }
-      }
-    }
-
-    /* May need to adjust last_free_column after previous operation. */
-    while (last_free_column > 0 && columns[last_free_column-1] == -1)
-      last_free_column--;
-
-    /* If there are any activities starting at this time, add them to
-     * the right hand end of the columns list.
-     */
-    for (i = 0; i < nr_activities; ++i) {
-      if (activities[i].t == t)
-        columns[last_free_column++] = i;
-    }
-
-    /* Draw the line. */
-    ansi_blue ();
-    printf ("%6.1fms: ", t / 1000000);
-
-    ansi_magenta ();
-    for (j = 0; j < last_free_column; ++j) {
-      if (columns[j] >= 0) {
-        if (activities[columns[j]].t == t)
-          printf ("▲ ");
-        else if (activities[columns[j]].end_t == t)
-          printf ("▼ ");
-        else
-          printf ("│ ");
-      }
-      else
-        printf ("  ");
-    }
-    ansi_restore ();
-
-    for (j = 0; j < last_free_column; ++j) {
-      if (columns[j] >= 0 && activities[columns[j]].t == t) /* ▲ */
-        print_activity (&activities[columns[j]]);
-    }
-
-    printf ("\n");
-  }
-}
-
-static int
-compare_activities_pointers_by_mean (const void *av, const void *bv)
-{
-  const struct activity * const *a = av;
-  const struct activity * const *b = bv;
-
-  return (*b)->mean - (*a)->mean;
-}
-
-static void
-print_longest_to_shortest (void)
-{
-  size_t i;
-  CLEANUP_FREE struct activity **longest;
-
-  /* Sort the activities longest first.  In order not to affect the
-   * global activities array, sort an array of pointers to the
-   * activities instead.
-   */
-  longest = malloc (sizeof (struct activity *) * nr_activities);
-  for (i = 0; i < nr_activities; ++i)
-    longest[i] = &activities[i];
-
-  qsort (longest, nr_activities, sizeof (struct activity *),
-         compare_activities_pointers_by_mean);
-
-  /* Display the activities, longest first. */
-  for (i = 0; i < nr_activities; ++i) {
-    print_activity (longest[i]);
-    printf ("\n");
-  }
-}
-
-/* Free the non-static part of the pass_data structures. */
-static void
-free_pass_data (void)
-{
-  size_t i, j;
-
-  for (i = 0; i < NR_TEST_PASSES; ++i) {
-    for (j = 0; j < pass_data[i].nr_events; ++j)
-      free (pass_data[i].events[j].message);
-    free (pass_data[i].events);
-  }
-}
-
-static void
-free_final_timeline (void)
-{
-  size_t i;
-
-  for (i = 0; i < nr_activities; ++i)
-    free (activities[i].name);
-  free (activities);
-}
-
-/* Colours. */
-static void
-ansi_green (void)
-{
-  if (force_colour || isatty (1))
-    fputs ("\033[0;32m", stdout);
-}
-
-static void
-ansi_red (void)
-{
-  if (force_colour || isatty (1))
-    fputs ("\033[1;31m", stdout);
-}
-
-static void
-ansi_blue (void)
-{
-  if (force_colour || isatty (1))
-    fputs ("\033[1;34m", stdout);
-}
-
-static void
-ansi_magenta (void)
-{
-  if (force_colour || isatty (1))
-    fputs ("\033[1;35m", stdout);
-}
-
-static void
-ansi_restore (void)
-{
-  if (force_colour || isatty (1))
-    fputs ("\033[0m", stdout);
-}
diff --git a/tests/qemu/boot-analysis.h b/tests/qemu/boot-analysis.h
deleted file mode 100644
index a07f12e..0000000
--- a/tests/qemu/boot-analysis.h
+++ /dev/null
@@ -1,102 +0,0 @@
-/* libguestfs
- * Copyright (C) 2016 Red Hat Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#ifndef GUESTFS_BOOT_ANALYSIS_H_
-#define GUESTFS_BOOT_ANALYSIS_H_
-
-#define NR_WARMUP_PASSES 3
-#define NR_TEST_PASSES   5
-
-/* Per-pass data collected. */
-struct pass_data {
-  size_t pass;
-  struct timespec start_t;
-  struct timespec end_t;
-  int64_t elapsed_ns;
-
-  /* Array of timestamped events. */
-  size_t nr_events;
-  struct event *events;
-
-  /* Was the previous appliance log message incomplete?  If so, this
-   * contains the index of that incomplete message in the events
-   * array.
-   */
-  ssize_t incomplete_log_message;
-
-  /* Have we seen the launch event yet?  We don't record events until
-   * this one has been received.  This makes it easy to base the
-   * timeline at event 0.
-   */
-  int seen_launch;
-};
-
-/* The 'source' field in the event is a guestfs event
- * (GUESTFS_EVENT_*).  We also wish to encode libvirt as a source, so
- * we use a magic/impossible value for that here.  Note that events
- * are bitmasks, and normally no more than one bit may be set.
- */
-#define SOURCE_LIBVIRT ((uint64_t)~0)
-extern char *source_to_string (uint64_t source);
-
-struct event {
-  struct timespec t;
-  uint64_t source;
-  char *message;
-};
-
-extern struct pass_data pass_data[NR_TEST_PASSES];
-
-/* The final timeline consisting of various activities starting and
- * ending.  We're interested in when the activities start, and how
- * long they take (mean, variance, standard deviation of length).
- */
-struct activity {
-  char *name;                   /* Name of this activity. */
-  int flags;
-#define LONG_ACTIVITY 1         /* Expected to take a long time. */
-
-  /* For each pass, record the actual start & end events of this
-   * activity.
-   */
-  size_t start_event[NR_TEST_PASSES];
-  size_t end_event[NR_TEST_PASSES];
-
-  double t;                     /* Start (ns offset). */
-  double end_t;                 /* t + mean - 1 */
-
-  /* Length of this activity. */
-  double mean;                  /* Mean time elapsed (ns). */
-  double variance;              /* Variance. */
-  double sd;                    /* Standard deviation. */
-  double percent;               /* Percent of total elapsed time. */
-
-  int warning;                  /* Appears in red. */
-};
-
-extern size_t nr_activities;
-extern struct activity *activities;
-
-extern int activity_exists (const char *name);
-extern struct activity *add_activity (const char *name, int flags);
-extern struct activity *find_activity (const char *name);
-extern int activity_exists_with_no_data (const char *name, size_t pass);
-
-extern void construct_timeline (void);
-
-#endif /* GUESTFS_BOOT_ANALYSIS_H_ */
diff --git a/tests/qemu/boot-benchmark-range.pl b/tests/qemu/boot-benchmark-range.pl
deleted file mode 100755
index 0e31c4d..0000000
--- a/tests/qemu/boot-benchmark-range.pl
+++ /dev/null
@@ -1,240 +0,0 @@
-#!/usr/bin/env perl
-# Copyright (C) 2016 Red Hat Inc.
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-use warnings;
-use strict;
-
-use Pod::Usage;
-use Getopt::Long;
-
-=head1 NAME
-
-boot-benchmark-range.pl - Benchmark libguestfs across a range of commits
-from another project
-
-=head1 SYNOPSIS
-
- LIBGUESTFS_BACKEND=direct \
- LIBGUESTFS_HV=/path/to/qemu/x86_64-softmmu/qemu-system-x86_64 \
-   ./run \
-   tests/qemu/boot-benchmark-range.pl /path/to/qemu HEAD~50..HEAD
-
-=head1
-
-Run F<tests/qemu/boot-benchmark> across a range of commits in another
-project.  This is useful for finding performance regressions in other
-programs such as qemu or the Linux kernel which might be affecting
-libguestfs.
-
-For example, suppose you suspect there has been a performance
-regression in qemu, somewhere between C<HEAD~50..HEAD>.  You could run
-the script like this:
-
- LIBGUESTFS_BACKEND=direct \
- LIBGUESTFS_HV=/path/to/qemu/x86_64-softmmu/qemu-system-x86_64 \
-   ./run \
-   tests/qemu/boot-benchmark-range.pl /path/to/qemu HEAD~50..HEAD
-
-where F</path/to/qemu> is the path to the qemu git repository.
-
-The output is a list of the qemu commits, annotated by the benchmark
-time and some other information about how the time compares to the
-previous commit.
-
-You should run these tests on an unloaded machine.  In particular
-running a desktop environment, web browser and so on can make the
-benchmarks useless.
-
-=head1 OPTIONS
-
-=over 4
-
-=cut
-
-my $help;
-
-=item B<--help>
-
-Display brief help.
-
-=cut
-
-my $man;
-
-=item B<--man>
-
-Display full documentation (man page).
-
-=cut
-
-my $benchmark_command;
-
-=item B<--benchmark> C<boot-benchmark>
-
-Set the name of the benchmark to run.  You only need to use this if
-the script cannot find the right path to the libguestfs
-F<tests/qemu/boot-benchmark> program.  By default the script looks for
-this file in the same directory as its executable.
-
-=cut
-
-my $make_command = "make";
-
-=item B<--make> C<make>
-
-Set the command used to build the other project.  The default is
-to run C<make>.
-
-If the command fails, then the commit is skipped.
-
-=back
-
-=cut
-
-# Clean up the program name.
-my $progname = $0;
-$progname =~ s{.*/}{};
-
-# Parse options.
-GetOptions ("help|?" => \$help,
-            "man" => \$man,
-            "benchmark=s" => \$benchmark_command,
-            "make=s" => \$make_command,
-    ) or pod2usage (2);
-pod2usage (-exitval => 0) if $help;
-pod2usage (-exitval => 0, -verbose => 2) if $man;
-
-die "$progname: missing argument: requires path to git repository and range of commits\n" unless @ARGV == 2;
-
-my $dir = $ARGV[0];
-my $range = $ARGV[1];
-
-die "$progname: $dir is not a git repository\n"
-    unless -d $dir && -d "$dir/.git";
-
-sub silently_run
-{
-    open my $saveout, ">&STDOUT";
-    open my $saveerr, ">&STDERR";
-    open STDOUT, ">/dev/null";
-    open STDERR, ">/dev/null";
-    my $ret = system (@_);
-    open STDOUT, ">&", $saveout;
-    open STDERR, ">&", $saveerr;
-    return $ret;
-}
-
-# Find the benchmark program and check it works.
-unless (defined $benchmark_command) {
-    $benchmark_command = $0;
-    $benchmark_command =~ s{/[^/]+$}{};
-    $benchmark_command .= "/boot-benchmark";
-
-    my $r = silently_run ("$benchmark_command", "--help");
-    die "$progname: cannot locate boot-benchmark program, try using --benchmark\n" unless $r == 0;
-}
-
-# Get the top-most commit from the remote, and restore it on exit.
-my $top_commit = `git -C '$dir' rev-parse HEAD`;
-chomp $top_commit;
-
-sub checkout
-{
-    my $sha = shift;
-    my $ret = silently_run ("git", "-C", $dir, "checkout", $sha);
-    return $ret;
-}
-
-END {
-    checkout ($top_commit);
-}
-
-# Get the range of commits and log messages.
-my @range = ();
-open RANGE, "git -C '$dir' log --reverse --oneline $range |" or die;
-while (<RANGE>) {
-    if (m/^([0-9a-f]+) (.*)/) {
-        my $sha = $1;
-        my $msg = $2;
-        push @range, [ $sha, $msg ];
-    }
-}
-close RANGE or die;
-
-# Run the test.
-my $prev_ms;
-foreach (@range) {
-    my ($sha, $msg) = @$_;
-    my $r;
-
-    print "\n";
-    print "$sha $msg\n";
-
-    # Checkout this commit in the other repo.
-    $r = checkout ($sha);
-    if ($r != 0) {
-        print "git checkout failed\n";
-        next;
-    }
-
-    # Build the repo, silently.
-    $r = silently_run ("cd $dir && $make_command");
-    if ($r != 0) {
-        print "build failed\n";
-        next;
-    }
-
-    # Run the benchmark program and get the timing.
-    my ($time_ms, $time_str);
-    open BENCHMARK, "$benchmark_command | grep '^Result:' |" or die;
-    while (<BENCHMARK>) {
-        die unless m/^Result: (([\d.]+)ms ±[\d.]+ms)/;
-        $time_ms = $2;
-        $time_str = $1;
-    }
-    close BENCHMARK;
-
-    print "\t", $time_str;
-    if (defined $prev_ms) {
-        if ($prev_ms > $time_ms) {
-            my $pc = 100 * ($prev_ms-$time_ms) / $time_ms;
-            if ($pc >= 1) {
-                printf (" ↑ improves performance by %0.1f%%", $pc);
-            }
-        } elsif ($prev_ms < $time_ms) {
-            my $pc = 100 * ($time_ms-$prev_ms) / $prev_ms;
-            if ($pc >= 1) {
-                printf (" ↓ degrades performance by %0.1f%%", $pc);
-            }
-        }
-    }
-    print "\n";
-    $prev_ms = $time_ms;
-}
-
-=head1 SEE ALSO
-
-L<git(1)>,
-L<guestfs-performance(1)>.
-
-=head1 AUTHOR
-
-Richard W.M. Jones.
-
-=head1 COPYRIGHT
-
-Copyright (C) 2016 Red Hat Inc.
diff --git a/tests/qemu/boot-benchmark.c b/tests/qemu/boot-benchmark.c
deleted file mode 100644
index 0508ee9..0000000
--- a/tests/qemu/boot-benchmark.c
+++ /dev/null
@@ -1,225 +0,0 @@
-/* libguestfs
- * Copyright (C) 2016 Red Hat Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-/* Benchmark the time taken to boot the libguestfs appliance. */
-
-#include <config.h>
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdint.h>
-#include <inttypes.h>
-#include <string.h>
-#include <getopt.h>
-#include <limits.h>
-#include <time.h>
-#include <errno.h>
-#include <error.h>
-#include <assert.h>
-#include <math.h>
-
-#include "guestfs.h"
-#include "guestfs-internal-frontend.h"
-
-#include "boot-analysis-utils.h"
-
-#define NR_WARMUP_PASSES 3
-#define NR_TEST_PASSES   10
-
-static const char *append = NULL;
-static int memsize = 0;
-static int smp = 1;
-
-static void run_test (void);
-static guestfs_h *create_handle (void);
-static void add_drive (guestfs_h *g);
-
-static void
-usage (int exitcode)
-{
-  guestfs_h *g;
-  int default_memsize = -1;
-
-  g = guestfs_create ();
-  if (g) {
-    default_memsize = guestfs_get_memsize (g);
-    guestfs_close (g);
-  }
-
-  fprintf (stderr,
-           "boot-benchmark: Benchmark the time taken to boot the libguestfs appliance.\n"
-           "Usage:\n"
-           "  boot-benchmark [--options]\n"
-           "Options:\n"
-           "  --help         Display this usage text and exit.\n"
-           "  --append OPTS  Append OPTS to kernel command line.\n"
-           "  -m MB\n"
-           "  --memsize MB   Set memory size in MB (default: %d).\n"
-           "  --smp N        Enable N virtual CPUs (default: 1).\n",
-           default_memsize);
-  exit (exitcode);
-}
-
-int
-main (int argc, char *argv[])
-{
-  enum { HELP_OPTION = CHAR_MAX + 1 };
-  static const char *options = "m:";
-  static const struct option long_options[] = {
-    { "help", 0, 0, HELP_OPTION },
-    { "append", 1, 0, 0 },
-    { "memsize", 1, 0, 'm' },
-    { "smp", 1, 0, 0 },
-    { 0, 0, 0, 0 }
-  };
-  int c, option_index;
-
-  for (;;) {
-    c = getopt_long (argc, argv, options, long_options, &option_index);
-    if (c == -1) break;
-
-    switch (c) {
-    case 0:                     /* Options which are long only. */
-      if (STREQ (long_options[option_index].name, "append")) {
-        append = optarg;
-        break;
-      }
-      else if (STREQ (long_options[option_index].name, "smp")) {
-        if (sscanf (optarg, "%d", &smp) != 1) {
-          fprintf (stderr, "%s: could not parse smp parameter: %s\n",
-                   guestfs_int_program_name, optarg);
-          exit (EXIT_FAILURE);
-        }
-        break;
-      }
-      fprintf (stderr, "%s: unknown long option: %s (%d)\n",
-               guestfs_int_program_name, long_options[option_index].name, option_index);
-      exit (EXIT_FAILURE);
-
-    case 'm':
-      if (sscanf (optarg, "%d", &memsize) != 1) {
-        fprintf (stderr, "%s: could not parse memsize parameter: %s\n",
-                 guestfs_int_program_name, optarg);
-        exit (EXIT_FAILURE);
-      }
-      break;
-
-    case HELP_OPTION:
-      usage (EXIT_SUCCESS);
-
-    default:
-      usage (EXIT_FAILURE);
-    }
-  }
-
-  run_test ();
-}
-
-static void
-run_test (void)
-{
-  guestfs_h *g;
-  size_t i;
-  int64_t ns[NR_TEST_PASSES];
-  double mean;
-  double variance;
-  double sd;
-
-  printf ("Warming up the libguestfs cache ...\n");
-  for (i = 0; i < NR_WARMUP_PASSES; ++i) {
-    g = create_handle ();
-    add_drive (g);
-    if (guestfs_launch (g) == -1)
-      exit (EXIT_FAILURE);
-    guestfs_close (g);
-  }
-
-  printf ("Running the tests ...\n");
-  for (i = 0; i < NR_TEST_PASSES; ++i) {
-    struct timespec start_t, end_t;
-
-    g = create_handle ();
-    add_drive (g);
-    get_time (&start_t);
-    if (guestfs_launch (g) == -1)
-      exit (EXIT_FAILURE);
-    guestfs_close (g);
-    get_time (&end_t);
-
-    ns[i] = timespec_diff (&start_t, &end_t);
-  }
-
-  /* Calculate the mean. */
-  mean = 0;
-  for (i = 0; i < NR_TEST_PASSES; ++i)
-    mean += ns[i];
-  mean /= NR_TEST_PASSES;
-
-  /* Calculate the variance and standard deviation. */
-  variance = 0;
-  for (i = 0; i < NR_TEST_PASSES; ++i)
-    variance = pow (ns[i] - mean, 2);
-  variance /= NR_TEST_PASSES;
-  sd = sqrt (variance);
-
-  /* Print the test parameters. */
-  printf ("\n");
-  g = create_handle ();
-  test_info (g, NR_TEST_PASSES);
-  guestfs_close (g);
-
-  /* Print the result. */
-  printf ("\n");
-  printf ("Result: %.1fms ±%.1fms\n", mean / 1000000, sd / 1000000);
-}
-
-/* Common function to create the handle and set various defaults. */
-static guestfs_h *
-create_handle (void)
-{
-  guestfs_h *g;
-  CLEANUP_FREE char *full_append = NULL;
-
-  g = guestfs_create ();
-  if (!g) error (EXIT_FAILURE, errno, "guestfs_create");
-
-  if (memsize != 0)
-    if (guestfs_set_memsize (g, memsize) == -1)
-      exit (EXIT_FAILURE);
-
-  if (smp >= 2)
-    if (guestfs_set_smp (g, smp) == -1)
-      exit (EXIT_FAILURE);
-
-  if (append != NULL)
-    if (guestfs_set_append (g, full_append) == -1)
-      exit (EXIT_FAILURE);
-
-  return g;
-}
-
-/* Common function to add the /dev/null drive. */
-static void
-add_drive (guestfs_h *g)
-{
-  if (guestfs_add_drive_opts (g, "/dev/null",
-                              GUESTFS_ADD_DRIVE_OPTS_FORMAT, "raw",
-                              GUESTFS_ADD_DRIVE_OPTS_READONLY, 1,
-                              -1) == -1)
-    exit (EXIT_FAILURE);
-}
diff --git a/tests/qemu/qemu-boot.c b/tests/qemu/qemu-boot.c
deleted file mode 100644
index 336c26e..0000000
--- a/tests/qemu/qemu-boot.c
+++ /dev/null
@@ -1,362 +0,0 @@
-/* libguestfs
- * Copyright (C) 2014-2016 Red Hat Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-/* Ancient libguestfs had a script called test-bootbootboot which just
- * booted up the appliance in a loop.  This was necessary back in the
- * bad old days when qemu was not very reliable.  This is the
- * spiritual successor of that script, designed to find bugs in
- * aarch64 KVM.  You can control the number of boots that are done and
- * the amount of parallelism.
- */
-
-#include <config.h>
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <getopt.h>
-#include <limits.h>
-#include <errno.h>
-#include <pthread.h>
-
-#include "guestfs.h"
-#include "guestfs-internal-frontend.h"
-#include "estimate-max-threads.h"
-
-#define MIN(a,b) ((a)<(b)?(a):(b))
-
-/* Maximum number of threads we would ever run.  Note this should not
- * be > 20, unless libvirt is modified to increase the maximum number
- * of clients.  User can override this limit using -P.
- */
-#define MAX_THREADS 12
-
-static size_t n;       /* Number of qemu processes to run in total. */
-static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
-
-static int ignore_errors = 0;
-static const char *log_template = NULL;
-static size_t log_file_size;
-static int trace = 0;
-static int verbose = 0;
-
-/* Events captured by the --log option. */
-static const uint64_t event_bitmask =
-  GUESTFS_EVENT_LIBRARY |
-  GUESTFS_EVENT_WARNING |
-  GUESTFS_EVENT_APPLIANCE |
-  GUESTFS_EVENT_TRACE;
-
-struct thread_data {
-  int thread_num;
-  int r;
-};
-
-static void *start_thread (void *thread_data_vp);
-static void message_callback (guestfs_h *g, void *opaque, uint64_t event, int event_handle, int flags, const char *buf, size_t buf_len, const uint64_t *array, size_t array_len);
-
-static void
-usage (int exitcode)
-{
-  fprintf (stderr,
-           "qemu-boot: A program for repeatedly running the libguestfs appliance.\n"
-           "qemu-boot [-i] [--log output.%%] [-P <nr-threads>] -n <nr-appliances>\n"
-           "  -i     Ignore errors\n"
-           "  --log <file.%%>\n"
-           "         Write per-appliance logs to file (%% in name replaced by boot number)\n"
-           "  -P <n> Set number of parallel threads\n"
-           "           (default is based on the amount of free memory)\n"
-           "  -n <n> Set number of appliances to run before exiting\n"
-           "  -v     Verbose appliance\n"
-           "  -x     Enable libguestfs tracing\n");
-  exit (exitcode);
-}
-
-int
-main (int argc, char *argv[])
-{
-  enum { HELP_OPTION = CHAR_MAX + 1 };
-  static const char options[] = "in:P:vx";
-  static const struct option long_options[] = {
-    { "help", 0, 0, HELP_OPTION },
-    { "ignore", 0, 0, 'i' },
-    { "log", 1, 0, 0 },
-    { "number", 1, 0, 'n' },
-    { "processes", 1, 0, 'P' },
-    { "trace", 0, 0, 'x' },
-    { "verbose", 0, 0, 'v' },
-    { 0, 0, 0, 0 }
-  };
-  size_t P = 0, i, errors;
-  int c, option_index;
-  int err;
-  void *status;
-
-  for (;;) {
-    c = getopt_long (argc, argv, options, long_options, &option_index);
-    if (c == -1) break;
-
-    switch (c) {
-    case 0:
-      /* Options which are long only. */
-      if (STREQ (long_options[option_index].name, "log")) {
-        log_template = optarg;
-        log_file_size = strlen (log_template);
-        for (i = 0; i < strlen (log_template); ++i) {
-          if (log_template[i] == '%')
-            log_file_size += 64;
-        }
-      }
-      else {
-        fprintf (stderr, "%s: unknown long option: %s (%d)\n",
-                 guestfs_int_program_name, long_options[option_index].name, option_index);
-        exit (EXIT_FAILURE);
-      }
-      break;
-
-    case 'i':
-      ignore_errors = 1;
-      break;
-
-    case 'n':
-      if (sscanf (optarg, "%zu", &n) != 1 || n == 0) {
-        fprintf (stderr, "%s: -n option not numeric and greater than 0\n",
-                 guestfs_int_program_name);
-        exit (EXIT_FAILURE);
-      }
-      break;
-
-    case 'P':
-      if (sscanf (optarg, "%zu", &P) != 1) {
-        fprintf (stderr, "%s: -P option not numeric\n", guestfs_int_program_name);
-        exit (EXIT_FAILURE);
-      }
-      break;
-
-    case 'v':
-      verbose = 1;
-      break;
-
-    case 'x':
-      trace = 1;
-      break;
-
-    case HELP_OPTION:
-      usage (EXIT_SUCCESS);
-
-    default:
-      usage (EXIT_FAILURE);
-    }
-  }
-
-  if (n == 0) {
-    fprintf (stderr,
-             "%s: must specify number of processes to run (-n option)\n",
-             guestfs_int_program_name);
-    exit (EXIT_FAILURE);
-  }
-
-  if (optind != argc) {
-    fprintf (stderr, "%s: extra arguments found on the command line\n",
-             guestfs_int_program_name);
-    exit (EXIT_FAILURE);
-  }
-
-  /* Calculate the number of threads to use. */
-  if (P > 0)
-    P = MIN (n, P);
-  else
-    P = MIN (n, MIN (MAX_THREADS, estimate_max_threads ()));
-
-  /* Start the worker threads. */
-  struct thread_data thread_data[P];
-  pthread_t threads[P];
-
-  for (i = 0; i < P; ++i) {
-    thread_data[i].thread_num = i;
-    err = pthread_create (&threads[i], NULL, start_thread, &thread_data[i]);
-    if (err != 0) {
-      fprintf (stderr, "%s: pthread_create[%zu]: %s\n",
-               guestfs_int_program_name, i, strerror (err));
-      exit (EXIT_FAILURE);
-    }
-  }
-
-  /* Wait for the threads to exit. */
-  errors = 0;
-  for (i = 0; i < P; ++i) {
-    err = pthread_join (threads[i], &status);
-    if (err != 0) {
-      fprintf (stderr, "%s: pthread_join[%zu]: %s\n",
-               guestfs_int_program_name, i, strerror (err));
-      errors++;
-    }
-    if (*(int *)status == -1)
-      errors++;
-  }
-
-  exit (errors == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
-}
-
-/* Worker thread. */
-static void *
-start_thread (void *thread_data_vp)
-{
-  struct thread_data *thread_data = thread_data_vp;
-  int quit = 0;
-  int err;
-  size_t i;
-  guestfs_h *g;
-  unsigned errors = 0;
-  char id[64];
-
-  for (;;) {
-    CLEANUP_FREE char *log_file = NULL;
-    CLEANUP_FCLOSE FILE *log_fp = NULL;
-
-    /* Take the next process. */
-    err = pthread_mutex_lock (&mutex);
-    if (err != 0) {
-      fprintf (stderr, "%s: pthread_mutex_lock: %s",
-               guestfs_int_program_name, strerror (err));
-      goto error;
-    }
-
-    i = n;
-    if (i > 0) {
-      printf ("%zu to go ...          \r", n);
-      fflush (stdout);
-
-      n--;
-    }
-    else
-      quit = 1;
-
-    err = pthread_mutex_unlock (&mutex);
-    if (err != 0) {
-      fprintf (stderr, "%s: pthread_mutex_unlock: %s",
-               guestfs_int_program_name, strerror (err));
-      goto error;
-    }
-
-    if (quit)                   /* Work finished. */
-      break;
-
-    g = guestfs_create ();
-    if (g == NULL) {
-      perror ("guestfs_create");
-      errors++;
-      if (!ignore_errors)
-        goto error;
-    }
-
-    /* Only if using --log, set up a callback.  See examples/debug-logging.c */
-    if (log_template != NULL) {
-      size_t j, k;
-
-      log_file = malloc (log_file_size + 1);
-      if (log_file == NULL) abort ();
-      for (j = 0, k = 0; j < strlen (log_template); ++j) {
-        if (log_template[j] == '%') {
-          snprintf (&log_file[k], log_file_size - k, "%zu", i);
-          k += strlen (&log_file[k]);
-        }
-        else
-          log_file[k++] = log_template[j];
-      }
-      log_file[k] = '\0';
-      log_fp = fopen (log_file, "w");
-      if (log_fp == NULL) {
-        perror (log_file);
-        abort ();
-      }
-      guestfs_set_event_callback (g, message_callback,
-                                  event_bitmask, 0, log_fp);
-    }
-
-    snprintf (id, sizeof id, "%zu", i);
-    guestfs_set_identifier (g, id);
-
-    guestfs_set_trace (g, trace);
-    guestfs_set_verbose (g, verbose);
-
-    if (guestfs_add_drive_ro (g, "/dev/null") == -1) {
-      errors++;
-      if (!ignore_errors)
-        goto error;
-    }
-
-    if (guestfs_launch (g) == -1) {
-      errors++;
-      if (!ignore_errors)
-        goto error;
-    }
-
-    if (guestfs_shutdown (g) == -1) {
-      errors++;
-      if (!ignore_errors)
-        goto error;
-    }
-
-    guestfs_close (g);
-  }
-
-  if (errors > 0) {
-    fprintf (stderr, "%s: thread %d: %u errors were ignored\n",
-             guestfs_int_program_name, thread_data->thread_num, errors);
-    goto error;
-  }
-
-  thread_data->r = 0;
-  return &thread_data->r;
-
- error:
-  thread_data->r = -1;
-  return &thread_data->r;
-}
-
-/* If using --log, this is called to write messages to the log file. */
-static void
-message_callback (guestfs_h *g, void *opaque,
-                  uint64_t event, int event_handle,
-                  int flags,
-                  const char *buf, size_t buf_len,
-                  const uint64_t *array, size_t array_len)
-{
-  FILE *fp = opaque;
-
-  if (buf_len > 0) {
-    CLEANUP_FREE char *msg = strndup (buf, buf_len);
-
-    switch (event) {
-    case GUESTFS_EVENT_APPLIANCE:
-      fprintf (fp, "%s", msg);
-      break;
-    case GUESTFS_EVENT_LIBRARY:
-      fprintf (fp, "libguestfs: %s\n", msg);
-      break;
-    case GUESTFS_EVENT_WARNING:
-      fprintf (fp, "libguestfs: warning: %s\n", msg);
-      break;
-    case GUESTFS_EVENT_TRACE:
-      fprintf (fp, "libguestfs: trace: %s\n", msg);
-      break;
-    }
-    fflush (fp);
-  }
-}
diff --git a/tests/qemu/qemu-speed-test.c b/tests/qemu/qemu-speed-test.c
deleted file mode 100644
index d5e34c3..0000000
--- a/tests/qemu/qemu-speed-test.c
+++ /dev/null
@@ -1,480 +0,0 @@
-/* libguestfs
- * Copyright (C) 2014 Red Hat Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-/* Test the speed of various qemu features.  Currently tested are:
- *   - virtio-serial upload
- *   - virtio-serial download
- *   - block device read
- *   - block device write
- * More to come in future.
- */
-
-#include <config.h>
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <inttypes.h>
-#include <errno.h>
-#include <getopt.h>
-#include <unistd.h>
-#include <signal.h>
-#include <assert.h>
-#include <sys/time.h>
-
-#include "guestfs.h"
-#include "guestfs-internal-frontend.h"
-
-static void test_virtio_serial (void);
-static void test_block_device (void);
-
-/* Which tests are enabled? -- All by default. */
-static int virtio_serial_upload = 1;
-static int virtio_serial_download = 1;
-static int block_device_write = 1;
-static int block_device_read = 1;
-
-static int max_time_override = 0;
-
-static void
-reset_default_tests (int *flag)
-{
-  if (*flag) {
-    virtio_serial_upload = 0;
-    virtio_serial_download = 0;
-    block_device_write = 0;
-    block_device_read = 0;
-    *flag = 0;
-  }
-}
-
-static void
-usage (int exitcode)
-{
-  fprintf (stderr,
-           "qemu-speed-test: Test the speed of qemu features.\n"
-           "\n"
-           "To run all tests (recommended), do:\n"
-           "  qemu-speed-test\n"
-           "\n"
-           "To run only specific tests, do:\n"
-           "  qemu-speed-test --option [--option ...]\n"
-           "where the test options are:\n"
-           "  --virtio-serial-upload\n"
-           "  --virtio-serial-download\n"
-           "  --block-device-write\n"
-           "  --block-device-read\n"
-           "\n"
-           "Other options:\n"
-           "  --help                       Display help output and exit\n"
-           "  -t <SECS> | --time=<SECS>    Set max length of test in seconds\n"
-           );
-  exit (exitcode);
-}
-
-int
-main (int argc, char *argv[])
-{
-  enum { HELP_OPTION = CHAR_MAX + 1 };
-  static const char options[] = "t:";
-  static const struct option long_options[] = {
-    { "help", 0, 0, HELP_OPTION },
-    { "time", 1, 0, 't' },
-
-    /* Tests. */
-    { "virtio-serial-upload", 0, 0, 0 },
-    { "virtio-serial-download", 0, 0, 0 },
-    { "block-device-write", 0, 0, 0 },
-    { "block-device-read", 0, 0, 0 },
-
-    { 0, 0, 0, 0 }
-  };
-  int c, option_index;
-  int reset_flag = 1;
-
-  for (;;) {
-    c = getopt_long (argc, argv, options, long_options, &option_index);
-    if (c == -1) break;
-
-    switch (c) {
-    case 0:
-      /* Options which are long only. */
-      if (STREQ (long_options[option_index].name, "virtio-serial-upload")) {
-        reset_default_tests (&reset_flag);
-        virtio_serial_upload = 1;
-      }
-      else if (STREQ (long_options[option_index].name, "virtio-serial-download")) {
-        reset_default_tests (&reset_flag);
-        virtio_serial_download = 1;
-      }
-      else if (STREQ (long_options[option_index].name, "block-device-write")) {
-        reset_default_tests (&reset_flag);
-        block_device_write = 1;
-      }
-      else if (STREQ (long_options[option_index].name, "block-device-read")) {
-        reset_default_tests (&reset_flag);
-        block_device_read = 1;
-      }
-      else {
-        fprintf (stderr, "%s: unknown long option: %s (%d)\n",
-                 guestfs_int_program_name, long_options[option_index].name, option_index);
-        exit (EXIT_FAILURE);
-      }
-      break;
-
-    case 't':
-      if (sscanf (optarg, "%d", &max_time_override) != 1 ||
-          max_time_override < 0) {
-        fprintf (stderr, "%s: -t: argument is not a positive integer\n",
-                 guestfs_int_program_name);
-        exit (EXIT_FAILURE);
-      }
-      break;
-
-    case HELP_OPTION:
-      usage (EXIT_SUCCESS);
-
-    default:
-      usage (EXIT_FAILURE);
-    }
-  }
-
-  if (optind != argc) {
-    fprintf (stderr, "%s: extra arguments found on the command line\n",
-             guestfs_int_program_name);
-    exit (EXIT_FAILURE);
-  }
-
-  test_virtio_serial ();
-  test_block_device ();
-
-  exit (EXIT_SUCCESS);
-}
-
-static void
-print_rate (const char *msg, int64_t rate)
-{
-  printf ("%-40s %" PRIi64 " bytes/sec (%" PRIi64 " Mbytes/sec)\n",
-          msg, rate, rate / 1024 / 1024);
-  fflush (stdout);
-}
-
-/* The maximum time we will spend running the test (seconds). */
-#define TEST_SERIAL_MAX_TIME 30
-
-/* The maximum amount of data to copy.  You can safely make this very
- * large because it's only making sparse files.
- */
-#define TEST_SERIAL_MAX_SIZE						\
-  (INT64_C(1024) * INT64_C(1024) * INT64_C(1024) * INT64_C(1024))
-
-static guestfs_h *g;
-static struct timeval start;
-static const char *operation;
-static int64_t rate;
-
-static void
-stop_transfer (int sig)
-{
-  guestfs_user_cancel (g);
-}
-
-/* Compute Y - X and return the result in milliseconds.
- * Approximately the same as this code:
- * http://www.mpp.mpg.de/~huber/util/timevaldiff.c
- */
-static int64_t
-timeval_diff (const struct timeval *x, const struct timeval *y)
-{
-  int64_t msec;
-
-  msec = (y->tv_sec - x->tv_sec) * 1000;
-  msec += (y->tv_usec - x->tv_usec) / 1000;
-  return msec;
-}
-
-static void
-progress_cb (guestfs_h *g, void *vp, uint64_t event,
-             int eh, int flags,
-             const char *buf, size_t buflen,
-             const uint64_t *array, size_t arraylen)
-{
-  uint64_t transferred;
-  struct timeval now;
-  int64_t millis;
-
-  assert (event == GUESTFS_EVENT_PROGRESS);
-  assert (arraylen >= 4);
-
-  gettimeofday (&now, NULL);
-
-  /* Bytes transferred. */
-  transferred = array[2];
-
-  /* Calculate the speed of the upload or download. */
-  millis = timeval_diff (&start, &now);
-  assert (millis >= 0);
-
-  if (millis != 0) {
-    rate = 1000 * transferred / millis;
-    printf ("%s: %" PRIi64 " bytes/sec          \r",
-            operation, rate);
-    fflush (stdout);
-  }
-}
-
-static void
-test_virtio_serial (void)
-{
-  int fd, r, eh;
-  char tmpfile[] = "/tmp/speedtestXXXXXX";
-  struct sigaction sa, old_sa;
-
-  if (!virtio_serial_upload && !virtio_serial_download)
-    return;
-
-  /* Create a sparse file.  We could upload from /dev/zero, but we
-   * won't get progress messages because libguestfs tests if the
-   * source file is a regular file.
-   */
-  fd = mkstemp (tmpfile);
-  if (fd == -1) {
-    perror ("mkstemp");
-    exit (EXIT_FAILURE);
-  }
-  if (ftruncate (fd, TEST_SERIAL_MAX_SIZE) == -1) {
-    perror ("ftruncate");
-    exit (EXIT_FAILURE);
-  }
-  if (close (fd) == -1) {
-    perror ("close");
-    exit (EXIT_FAILURE);
-  }
-
-  g = guestfs_create ();
-  if (!g) {
-    perror ("guestfs_create");
-    exit (EXIT_FAILURE);
-  }
-
-  if (guestfs_add_drive_scratch (g, INT64_C (100*1024*1024), -1) == -1)
-    exit (EXIT_FAILURE);
-
-  if (guestfs_launch (g) == -1)
-    exit (EXIT_FAILURE);
-
-  /* Make and mount a filesystem which will be used by the download test. */
-  if (guestfs_mkfs (g, "ext4", "/dev/sda") == -1)
-    exit (EXIT_FAILURE);
-  if (guestfs_mount (g, "/dev/sda", "/") == -1)
-    exit (EXIT_FAILURE);
-
-  /* Time out the upload after TEST_SERIAL_MAX_TIME seconds have passed. */
-  memset (&sa, 0, sizeof sa);
-  sa.sa_handler = stop_transfer;
-  sa.sa_flags = SA_RESTART;
-  sigaction (SIGALRM, &sa, &old_sa);
-
-  /* Get progress messages, which will tell us how much data has been
-   * transferred.
-   */
-  eh = guestfs_set_event_callback (g, progress_cb, GUESTFS_EVENT_PROGRESS,
-                                   0, NULL);
-  if (eh == -1)
-    exit (EXIT_FAILURE);
-
-  if (virtio_serial_upload) {
-    gettimeofday (&start, NULL);
-    rate = -1;
-    operation = "upload";
-    alarm (max_time_override > 0 ? max_time_override : TEST_SERIAL_MAX_TIME);
-
-    /* For the upload test, upload the sparse file to /dev/null in the
-     * appliance.  Hopefully this is mostly testing just virtio-serial.
-     */
-    guestfs_push_error_handler (g, NULL, NULL);
-    r = guestfs_upload (g, tmpfile, "/dev/null");
-    alarm (0);
-    unlink (tmpfile);
-    guestfs_pop_error_handler (g);
-
-    /* It's possible that the upload will finish before the alarm fires,
-     * or that the upload will be stopped by the alarm.
-     */
-    if (r == -1 && guestfs_last_errno (g) != EINTR) {
-      fprintf (stderr,
-               "%s: expecting upload command to return EINTR\n%s\n",
-               guestfs_int_program_name, guestfs_last_error (g));
-      exit (EXIT_FAILURE);
-    }
-
-    if (rate == -1) {
-    rate_error:
-      fprintf (stderr, "%s: internal error: progress callback was not called! (r=%d, errno=%d)\n",
-               guestfs_int_program_name,
-               r, guestfs_last_errno (g));
-      exit (EXIT_FAILURE);
-    }
-
-    print_rate ("virtio-serial upload rate:", rate);
-  }
-
-  if (virtio_serial_download) {
-    /* For the download test, download a sparse file within the
-     * appliance to /dev/null on the host.
-     */
-    if (guestfs_touch (g, "/sparse") == -1)
-      exit (EXIT_FAILURE);
-    if (guestfs_truncate_size (g, "/sparse", TEST_SERIAL_MAX_SIZE) == -1)
-      exit (EXIT_FAILURE);
-
-    gettimeofday (&start, NULL);
-    rate = -1;
-    operation = "download";
-    alarm (max_time_override > 0 ? max_time_override : TEST_SERIAL_MAX_TIME);
-    guestfs_push_error_handler (g, NULL, NULL);
-    r = guestfs_download (g, "/sparse", "/dev/null");
-    alarm (0);
-    guestfs_pop_error_handler (g);
-
-    if (r == -1 && guestfs_last_errno (g) != EINTR) {
-      fprintf (stderr,
-               "%s: expecting download command to return EINTR\n%s\n",
-               guestfs_int_program_name, guestfs_last_error (g));
-      exit (EXIT_FAILURE);
-    }
-
-    if (rate == -1)
-      goto rate_error;
-
-    print_rate ("virtio-serial download rate:", rate);
-  }
-
-  if (guestfs_shutdown (g) == -1)
-    exit (EXIT_FAILURE);
-
-  guestfs_close (g);
-
-  /* Restore SIGALRM signal handler. */
-  sigaction (SIGALRM, &old_sa, NULL);
-}
-
-/* The time we will spend running the test (seconds). */
-#define TEST_BLOCK_DEVICE_TIME 30
-
-static void
-test_block_device (void)
-{
-  int fd;
-  char tmpfile[] = "/tmp/speedtestXXXXXX";
-  CLEANUP_FREE char **devices = NULL;
-  char *r;
-  const char *argv[4];
-  const int t =
-    max_time_override > 0 ? max_time_override : TEST_BLOCK_DEVICE_TIME;
-  char tbuf[64];
-  int64_t bytes_written, bytes_read;
-
-  if (!block_device_write && !block_device_read)
-    return;
-
-  snprintf (tbuf, sizeof tbuf, "%d", t);
-
-  g = guestfs_create ();
-  if (!g) {
-    perror ("guestfs_create");
-    exit (EXIT_FAILURE);
-  }
-
-  /* Create a fully allocated backing file.  Note we are not testing
-   * the speed of allocation on the host.
-   */
-  fd = mkstemp (tmpfile);
-  if (fd == -1) {
-    perror ("mkstemp");
-    exit (EXIT_FAILURE);
-  }
-  close (fd);
-
-  if (guestfs_disk_create (g, tmpfile, "raw",
-                           INT64_C (1024*1024*1024),
-                           GUESTFS_DISK_CREATE_PREALLOCATION, "full",
-                           -1) == -1)
-    exit (EXIT_FAILURE);
-
-  if (guestfs_add_drive (g, tmpfile) == -1)
-    exit (EXIT_FAILURE);
-
-  if (guestfs_launch (g) == -1)
-    exit (EXIT_FAILURE);
-
-  devices = guestfs_list_devices (g);
-  if (devices == NULL)
-    exit (EXIT_FAILURE);
-  if (devices[0] == NULL) {
-    fprintf (stderr, "%s: expected guestfs_list_devices to return at least 1 device\n",
-             guestfs_int_program_name);
-    exit (EXIT_FAILURE);
-  }
-
-  if (block_device_write) {
-    /* Test write speed. */
-    argv[0] = devices[0];
-    argv[1] = "w";
-    argv[2] = tbuf;
-    argv[3] = NULL;
-    r = guestfs_debug (g, "device_speed", (char **) argv);
-    if (r == NULL)
-      exit (EXIT_FAILURE);
-
-    if (sscanf (r, "%" SCNi64, &bytes_written) != 1) {
-      fprintf (stderr, "%s: could not parse device_speed output\n",
-               guestfs_int_program_name);
-      exit (EXIT_FAILURE);
-    }
-
-    print_rate ("block device writes:", bytes_written / t);
-  }
-
-  if (block_device_read) {
-    /* Test read speed. */
-    argv[0] = devices[0];
-    argv[1] = "r";
-    argv[2] = tbuf;
-    argv[3] = NULL;
-    r = guestfs_debug (g, "device_speed", (char **) argv);
-    if (r == NULL)
-      exit (EXIT_FAILURE);
-
-    if (sscanf (r, "%" SCNi64, &bytes_read) != 1) {
-      fprintf (stderr, "%s: could not parse device_speed output\n",
-               guestfs_int_program_name);
-      exit (EXIT_FAILURE);
-    }
-
-    print_rate ("block device reads:", bytes_read / t);
-  }
-
-  if (guestfs_shutdown (g) == -1)
-    exit (EXIT_FAILURE);
-
-  guestfs_close (g);
-
-  /* Remove temporary file. */
-  unlink (tmpfile);
-}
diff --git a/utils/boot-analysis/Makefile.am b/utils/boot-analysis/Makefile.am
new file mode 100644
index 0000000..ef9b2cb
--- /dev/null
+++ b/utils/boot-analysis/Makefile.am
@@ -0,0 +1,43 @@
+# libguestfs
+# Copyright (C) 2011-2016 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+include $(top_srcdir)/subdir-rules.mk
+
+noinst_PROGRAMS = boot-analysis
+
+boot_analysis_SOURCES = \
+	boot-analysis.c \
+	boot-analysis.h \
+	boot-analysis-timeline.c \
+	boot-analysis-utils.c \
+	boot-analysis-utils.h
+boot_analysis_CPPFLAGS = \
+	-I$(top_srcdir)/gnulib/lib -I$(top_builddir)/gnulib/lib \
+	-I$(top_srcdir)/src -I$(top_builddir)/src
+boot_analysis_CFLAGS = \
+	-pthread \
+	$(WARN_CFLAGS) $(WERROR_CFLAGS) \
+	$(PCRE_CFLAGS)
+boot_analysis_LDADD = \
+	$(top_builddir)/src/libutils.la \
+	$(top_builddir)/src/libguestfs.la \
+	$(PCRE_LIBS) \
+	$(LIBXML2_LIBS) \
+	$(LIBVIRT_LIBS) \
+	$(LTLIBINTL) \
+	$(top_builddir)/gnulib/lib/libgnu.la \
+	-lm
diff --git a/utils/boot-analysis/boot-analysis-timeline.c b/utils/boot-analysis/boot-analysis-timeline.c
new file mode 100644
index 0000000..09a78ef
--- /dev/null
+++ b/utils/boot-analysis/boot-analysis-timeline.c
@@ -0,0 +1,523 @@
+/* libguestfs
+ * Copyright (C) 2016 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <string.h>
+#include <unistd.h>
+#include <error.h>
+#include <errno.h>
+#include <assert.h>
+
+#include <pcre.h>
+
+#include "ignore-value.h"
+
+#include "guestfs.h"
+#include "guestfs-internal-frontend.h"
+
+#include "boot-analysis.h"
+
+COMPILE_REGEXP(re_initcall_calling_module,
+               "calling  ([_A-Za-z0-9]+)\\+.*\\[([_A-Za-z0-9]+)]", 0)
+COMPILE_REGEXP(re_initcall_calling,
+               "calling  ([_A-Za-z0-9]+)\\+", 0)
+
+static void construct_initcall_timeline (void);
+
+/* "supermin: internal insmod xx.ko" -> "insmod xx.ko" */
+static char *
+translate_supermin_insmod_message (const char *message)
+{
+  char *ret;
+
+  assert (STRPREFIX (message, "supermin: internal "));
+
+  ret = strdup (message + strlen ("supermin: internal "));
+  if (ret == NULL)
+    error (EXIT_FAILURE, errno, "strdup");
+  return ret;
+}
+
+/* Analyze significant events from the events array, to form a
+ * timeline of activities.
+ */
+void
+construct_timeline (void)
+{
+  size_t i, j, k;
+  struct pass_data *data;
+  struct activity *activity;
+
+  for (i = 0; i < NR_TEST_PASSES; ++i) {
+    data = &pass_data[i];
+
+    /* Find an activity, by matching an event with the condition
+     * `begin_cond' through to the second event `end_cond'.  Create an
+     * activity object in the timeline from the result.
+     */
+#define FIND(name, flags, begin_cond, end_cond)                         \
+    do {                                                                \
+      activity = NULL;                                                  \
+      for (j = 0; j < data->nr_events; ++j) {                           \
+        if (begin_cond) {                                               \
+          for (k = j+1; k < data->nr_events; ++k) {                     \
+            if (end_cond) {                                             \
+              if (i == 0)                                               \
+                activity = add_activity (name, flags);                  \
+              else                                                      \
+                activity = find_activity (name);                        \
+              break;                                                    \
+            }                                                           \
+          }                                                             \
+          break;                                                        \
+        }                                                               \
+      }                                                                 \
+      if (activity) {                                                   \
+        activity->start_event[i] = j;                                   \
+        activity->end_event[i] = k;                                     \
+      }                                                                 \
+      else                                                              \
+        error (EXIT_FAILURE, 0, "could not find activity '%s' in pass '%zu'", \
+               name, i);                                                \
+    } while (0)
+
+    /* Same as FIND() macro, but if no matching events are found,
+     * ignore it.
+     */
+#define FIND_OPTIONAL(name, flags, begin_cond, end_cond)                \
+    do {                                                                \
+      activity = NULL;                                                  \
+      for (j = 0; j < data->nr_events; ++j) {                           \
+        if (begin_cond) {                                               \
+          for (k = j+1; k < data->nr_events; ++k) {                     \
+            if (end_cond) {                                             \
+              if (i == 0)                                               \
+                activity = add_activity (name, flags);                  \
+              else                                                      \
+                activity = find_activity (name);                        \
+              break;                                                    \
+            }                                                           \
+          }                                                             \
+          break;                                                        \
+        }                                                               \
+      }                                                                 \
+      if (activity) {                                                   \
+        activity->start_event[i] = j;                                   \
+        activity->end_event[i] = k;                                     \
+      }                                                                 \
+    } while (0)
+
+    /* Find multiple entries, where we check for:
+     *   next_cond
+     *   next_cond
+     *   next_cond
+     *   end_cond
+     */
+#define FIND_MULTIPLE(debug_name, flags, next_cond, end_cond, translate_message) \
+    do {                                                                \
+      activity = NULL;                                                  \
+      for (j = 0; j < data->nr_events; ++j) {                           \
+        if (next_cond) {                                                \
+          CLEANUP_FREE char *message = translate_message (data->events[j].message); \
+          if (activity)                                                 \
+            activity->end_event[i] = j;                                 \
+          if (i == 0)                                                   \
+            activity = add_activity (message, flags);                   \
+          else                                                          \
+            activity = find_activity (message);                         \
+          activity->start_event[i] = j;                                 \
+        }                                                               \
+        else if (end_cond)                                              \
+          break;                                                        \
+      }                                                                 \
+      if (j < data->nr_events && activity)                              \
+        activity->end_event[i] = j;                                     \
+      else                                                              \
+        error (EXIT_FAILURE, 0, "could not find activity '%s' in pass '%zu'", \
+               debug_name, i);                                          \
+    } while (0)
+
+    /* Add one activity which is going to cover the whole process
+     * from launch to close.  The launch event is always event 0.
+     * NB: This activity must be called "run" (see below).
+     */
+    FIND ("run", LONG_ACTIVITY,
+          j == 0, data->events[k].source == GUESTFS_EVENT_CLOSE);
+
+    /* Find where we invoke supermin --build.  This should be a null
+     * operation, but it still takes time to run the external command.
+     */
+    FIND ("supermin:build", 0,
+          data->events[j].source == GUESTFS_EVENT_LIBRARY &&
+          strstr (data->events[j].message,
+                  "begin building supermin appliance"),
+          data->events[k].source == GUESTFS_EVENT_LIBRARY &&
+          strstr (data->events[k].message,
+                  "finished building supermin appliance"));
+
+    /* Find where we invoke qemu to test features. */
+    FIND_OPTIONAL ("qemu:feature-detect", 0,
+                   data->events[j].source == GUESTFS_EVENT_LIBRARY &&
+                   strstr (data->events[j].message,
+                           "begin testing qemu features"),
+                   data->events[k].source == GUESTFS_EVENT_LIBRARY &&
+                   strstr (data->events[k].message,
+                           "finished testing qemu features"));
+
+    /* Find where we run qemu. */
+    FIND_OPTIONAL ("qemu", LONG_ACTIVITY,
+                   data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+                   strstr (data->events[j].message, "-nodefconfig"),
+                   data->events[k].source == GUESTFS_EVENT_CLOSE);
+
+    /* For the libvirt backend, connecting to libvirt, getting
+     * capabilities, parsing capabilities etc.
+     */
+    FIND_OPTIONAL ("libvirt:connect", 0,
+                   data->events[j].source == GUESTFS_EVENT_LIBRARY &&
+                   strstr (data->events[j].message, "connect to libvirt"),
+                   data->events[k].source == GUESTFS_EVENT_LIBRARY &&
+                   strstr (data->events[k].message, "successfully opened libvirt handle"));
+    FIND_OPTIONAL ("libvirt:get-libvirt-capabilities", 0,
+                   data->events[j].source == GUESTFS_EVENT_LIBRARY &&
+                   strstr (data->events[j].message, "get libvirt capabilities"),
+                   data->events[k].source == GUESTFS_EVENT_LIBRARY &&
+                   strstr (data->events[k].message, "parsing capabilities XML"));
+
+    FIND_OPTIONAL ("libguestfs:parse-libvirt-capabilities", 0,
+                   data->events[j].source == GUESTFS_EVENT_LIBRARY &&
+                   strstr (data->events[j].message, "parsing capabilities XML"),
+                   data->events[k].source == GUESTFS_EVENT_LIBRARY &&
+                   strstr (data->events[k].message, "get_backend_setting"));
+
+    FIND_OPTIONAL ("libguestfs:create-libvirt-xml", 0,
+                   data->events[j].source == GUESTFS_EVENT_LIBRARY &&
+                   strstr (data->events[j].message, "create libvirt XML"),
+                   data->events[k].source == GUESTFS_EVENT_LIBRARY &&
+                   strstr (data->events[k].message, "libvirt XML:"));
+
+#if defined(__aarch64__)
+#define FIRST_KERNEL_MESSAGE "Booting Linux on physical CPU"
+#define FIRST_FIRMWARE_MESSAGE "UEFI firmware starting"
+#else
+#define SGABIOS_STRING "\033[1;256r\033[256;256H\033[6n"
+#define FIRST_KERNEL_MESSAGE "Probing EDD"
+#define FIRST_FIRMWARE_MESSAGE SGABIOS_STRING
+#endif
+
+    /* For the libvirt backend, find the overhead of libvirt. */
+    FIND_OPTIONAL ("libvirt:overhead", 0,
+                   data->events[j].source == GUESTFS_EVENT_LIBRARY &&
+                   strstr (data->events[j].message, "launch libvirt guest"),
+                   data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+                   strstr (data->events[k].message, FIRST_FIRMWARE_MESSAGE));
+
+    /* From starting qemu up to entering the BIOS is the qemu overhead. */
+    FIND_OPTIONAL ("qemu:overhead", 0,
+                   data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+                   strstr (data->events[j].message, "-nodefconfig"),
+                   data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+                   strstr (data->events[k].message, FIRST_FIRMWARE_MESSAGE));
+
+    /* From entering the BIOS to starting the kernel is the BIOS overhead. */
+    FIND_OPTIONAL ("bios:overhead", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, FIRST_FIRMWARE_MESSAGE),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, FIRST_KERNEL_MESSAGE));
+
+#if defined(__i386__) || defined(__x86_64__)
+    /* SGABIOS (option ROM). */
+    FIND_OPTIONAL ("sgabios", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, SGABIOS_STRING),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "SeaBIOS (version"));
+#endif
+
+#if defined(__i386__) || defined(__x86_64__)
+    /* SeaBIOS. */
+    FIND ("seabios", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "SeaBIOS (version"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, FIRST_KERNEL_MESSAGE));
+#endif
+
+#if defined(__i386__) || defined(__x86_64__)
+    /* SeaBIOS - only available when using debug messages. */
+    FIND_OPTIONAL ("seabios:pci-probe", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "Searching bootorder for: /pci@"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "Scan for option roms"));
+#endif
+
+    /* Find where we run the guest kernel. */
+    FIND ("kernel", LONG_ACTIVITY,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, FIRST_KERNEL_MESSAGE),
+          data->events[k].source == GUESTFS_EVENT_CLOSE);
+
+    /* Kernel startup to userspace. */
+    FIND ("kernel:overhead", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, FIRST_KERNEL_MESSAGE),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "supermin:") &&
+          strstr (data->events[k].message, "starting up"));
+
+    /* The time taken to get into start_kernel function. */
+    FIND ("kernel:entry", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, FIRST_KERNEL_MESSAGE),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "Linux version"));
+
+#if defined(__i386__) || defined(__x86_64__)
+    /* Alternatives patching instructions (XXX not very accurate we
+     * really need some debug messages inserted into the code).
+     */
+    FIND ("kernel:alternatives", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "Last level dTLB entries"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "Freeing SMP alternatives"));
+#endif
+
+    /* ftrace patching instructions. */
+    FIND ("kernel:ftrace", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "ftrace: allocating"),
+          1);
+
+    /* All initcall functions, before we enter userspace. */
+    FIND ("kernel:initcalls-before-userspace", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "calling  "),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "Freeing unused kernel memory"));
+
+    /* Find where we run supermin mini-initrd. */
+    FIND ("supermin:mini-initrd", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "supermin:") &&
+          strstr (data->events[j].message, "starting up"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "supermin: chroot"));
+
+    /* Loading kernel modules from supermin initrd. */
+    FIND_MULTIPLE
+      ("supermin insmod", 0,
+       data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+       strstr (data->events[j].message, "supermin: internal insmod"),
+       data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+       strstr (data->events[j].message, "supermin: picked"),
+       translate_supermin_insmod_message);
+
+    /* Find where we run the /init script. */
+    FIND ("/init", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "supermin: chroot"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "guestfsd --verbose"));
+
+    /* Everything from the chroot to the first echo in the /init
+     * script counts as bash overhead.
+     */
+    FIND ("bash:overhead", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "supermin: chroot"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "Starting /init script"));
+
+    /* /init: Mount special filesystems. */
+    FIND ("/init:mount-special", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "*guestfs_boot_analysis=1*"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "kmod static-nodes"));
+
+    /* /init: Run kmod static-nodes */
+    FIND ("/init:kmod-static-nodes", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "kmod static-nodes"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "systemd-tmpfiles"));
+
+    /* /init: systemd-tmpfiles. */
+    FIND ("/init:systemd-tmpfiles", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "systemd-tmpfiles"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "udev"));
+
+    /* /init: start udevd. */
+    FIND ("/init:udev-overhead", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "udevd --daemon"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "nullglob"));
+
+    /* /init: set up network. */
+    FIND ("/init:network-overhead", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "+ ip addr"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "+ test"));
+
+    /* /init: probe MD arrays. */
+    FIND ("/init:md-probe", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "+ mdadm"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "+ modprobe dm_mod"));
+
+    /* /init: probe DM/LVM. */
+    FIND ("/init:lvm-probe", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "+ modprobe dm_mod"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "+ ldmtool"));
+
+    /* /init: probe Windows dynamic disks. */
+    FIND ("/init:windows-dynamic-disks-probe", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "+ ldmtool"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "+ test"));
+
+    /* Find where we run guestfsd. */
+    FIND ("guestfsd", 0,
+          data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[j].message, "guestfsd --verbose"),
+          data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+          strstr (data->events[k].message, "fsync /dev/sda"));
+
+    /* Shutdown process. */
+    FIND ("shutdown", 0,
+          data->events[j].source == GUESTFS_EVENT_TRACE &&
+          STREQ (data->events[j].message, "close"),
+          data->events[k].source == GUESTFS_EVENT_CLOSE);
+  }
+
+  construct_initcall_timeline ();
+}
+
+/* Handling of initcall is so peculiar that we hide it in a separate
+ * function from the rest.
+ */
+static void
+construct_initcall_timeline (void)
+{
+  size_t i, j, k;
+  struct pass_data *data;
+  struct activity *activity;
+
+  for (i = 0; i < NR_TEST_PASSES; ++i) {
+    data = &pass_data[i];
+
+    /* Each kernel initcall is bracketed by:
+     *
+     * calling  ehci_hcd_init+0x0/0xc1 @ 1"
+     * initcall ehci_hcd_init+0x0/0xc1 returned 0 after 420 usecs"
+     *
+     * For initcall functions in modules:
+     *
+     * calling  virtio_mmio_init+0x0/0x1000 [virtio_mmio] @ 1"
+     * initcall virtio_mmio_init+0x0/0x1000 [virtio_mmio] returned 0 after 14 usecs"
+     *
+     * Initcall functions can be nested, and do not have unique names.
+     */
+    for (j = 0; j < data->nr_events; ++j) {
+      int vec[30], r;
+      const char *message = data->events[j].message;
+
+      if (data->events[j].source == GUESTFS_EVENT_APPLIANCE &&
+          ((r = pcre_exec (re_initcall_calling_module, NULL,
+                           message, strlen (message),
+                           0, 0, vec, sizeof vec / sizeof vec[0])) >= 1 ||
+           (r = pcre_exec (re_initcall_calling, NULL,
+                           message, strlen (message),
+                           0, 0, vec, sizeof vec / sizeof vec[0])) >= 1)) {
+
+        CLEANUP_FREE char *fn_name = NULL, *module_name = NULL;
+        if (r >= 2) /* because pcre_exec returns 1 + number of captures */
+          fn_name = strndup (message + vec[2], vec[3]-vec[2]);
+        if (r >= 3)
+          module_name = strndup (message + vec[4], vec[5]-vec[4]);
+
+        CLEANUP_FREE char *fullname;
+        if (asprintf (&fullname, "%s.%s",
+                      module_name ? module_name : "kernel", fn_name) == -1)
+          error (EXIT_FAILURE, errno, "asprintf");
+
+        CLEANUP_FREE char *initcall_match;
+        if (asprintf (&initcall_match, "initcall %s", fn_name) == -1)
+          error (EXIT_FAILURE, errno, "asprintf");
+
+        /* Get a unique name for this activity.  Unfortunately
+         * kernel initcall function names are not unique!
+         */
+        CLEANUP_FREE char *activity_name;
+        if (asprintf (&activity_name, "initcall %s", fullname) == -1)
+          error (EXIT_FAILURE, errno, "asprintf");
+
+        if (i == 0) {
+          int n = 1;
+          while (activity_exists (activity_name)) {
+            free (activity_name);
+            if (asprintf (&activity_name, "initcall %s:%d", fullname, n) == -1)
+              error (EXIT_FAILURE, errno, "asprintf");
+            n++;
+          }
+        }
+        else {
+          int n = 1;
+          while (!activity_exists_with_no_data (activity_name, i)) {
+            free (activity_name);
+            if (asprintf (&activity_name, "initcall %s:%d", fullname, n) == -1)
+              error (EXIT_FAILURE, errno, "asprintf");
+            n++;
+          }
+        }
+
+        /* Find the matching end event.  It might be some time later,
+         * since it appears initcalls can be nested.
+         */
+        for (k = j+1; k < data->nr_events; ++k) {
+          if (data->events[k].source == GUESTFS_EVENT_APPLIANCE &&
+              strstr (data->events[k].message, initcall_match)) {
+            if (i == 0)
+              activity = add_activity (activity_name, 0);
+            else
+              activity = find_activity (activity_name);
+            activity->start_event[i] = j;
+            activity->end_event[i] = k;
+            break;
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/utils/boot-analysis/boot-analysis-utils.c b/utils/boot-analysis/boot-analysis-utils.c
new file mode 100644
index 0000000..693b6f4
--- /dev/null
+++ b/utils/boot-analysis/boot-analysis-utils.c
@@ -0,0 +1,90 @@
+/* libguestfs
+ * Copyright (C) 2016 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+#include <error.h>
+#include <errno.h>
+
+#include "ignore-value.h"
+
+#include "guestfs.h"
+#include "guestfs-internal-frontend.h"
+
+#include "boot-analysis-utils.h"
+
+void
+get_time (struct timespec *ts)
+{
+  if (clock_gettime (CLOCK_REALTIME, ts) == -1)
+    error (EXIT_FAILURE, errno, "clock_gettime: CLOCK_REALTIME");
+}
+
+int64_t
+timespec_diff (const struct timespec *x, const struct timespec *y)
+{
+  int64_t nsec;
+
+  nsec = (y->tv_sec - x->tv_sec) * UINT64_C(1000000000);
+  nsec += y->tv_nsec - x->tv_nsec;
+  return nsec;
+}
+
+void
+test_info (guestfs_h *g, int nr_test_passes)
+{
+  const char *qemu = guestfs_get_hv (g);
+  CLEANUP_FREE char *cmd = NULL;
+  CLEANUP_FREE char *backend = NULL;
+
+  /* Related to the test program. */
+  printf ("test version: %s %s\n", PACKAGE_NAME, PACKAGE_VERSION_FULL);
+  printf (" test passes: %d\n", nr_test_passes);
+
+  /* Related to the host. */
+  printf ("host version: ");
+  fflush (stdout);
+  ignore_value (system ("uname -a"));
+  printf ("    host CPU: ");
+  fflush (stdout);
+  ignore_value (system ("perl -n -e 'if (/^model name.*: (.*)/) { print \"$1\\n\"; exit }' /proc/cpuinfo"));
+
+  /* Related to qemu. */
+  backend = guestfs_get_backend (g);
+  printf ("     backend: %-20s [to change set LIBGUESTFS_BACKEND]\n",
+          backend);
+  printf ("        qemu: %-20s [to change set $LIBGUESTFS_HV]\n", qemu);
+  printf ("qemu version: ");
+  fflush (stdout);
+  if (asprintf (&cmd, "%s -version", qemu) == -1)
+    error (EXIT_FAILURE, errno, "asprintf");
+  ignore_value (system (cmd));
+  printf ("         smp: %-20d [to change use --smp option]\n",
+          guestfs_get_smp (g));
+  printf ("     memsize: %-20d [to change use --memsize option]\n",
+          guestfs_get_memsize (g));
+
+  /* Related to the guest kernel.  Be nice to get the guest
+   * kernel version here somehow (XXX).
+   */
+  printf ("      append: %-20s [to change use --append option]\n",
+          guestfs_get_append (g) ? : "");
+}
diff --git a/utils/boot-analysis/boot-analysis-utils.h b/utils/boot-analysis/boot-analysis-utils.h
new file mode 100644
index 0000000..95e4f06
--- /dev/null
+++ b/utils/boot-analysis/boot-analysis-utils.h
@@ -0,0 +1,36 @@
+/* libguestfs
+ * Copyright (C) 2016 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef GUESTFS_BOOT_ANALYSIS_UTILS_H_
+#define GUESTFS_BOOT_ANALYSIS_UTILS_H_
+
+/* Get current time, returning it in *ts.  If there is a system call
+ * failure, this exits.
+ */
+extern void get_time (struct timespec *ts);
+
+/* Computes Y - X, returning nanoseconds. */
+extern int64_t timespec_diff (const struct timespec *x, const struct timespec *y);
+
+/* Display host machine and test parameters (to stdout).  'g' should
+ * be an open libguestfs handle.  It is used for reading hv, memsize
+ * etc. and is not modified.
+ */
+extern void test_info (guestfs_h *g, int nr_test_passes);
+
+#endif /* GUESTFS_BOOT_ANALYSIS_UTILS_H_ */
diff --git a/utils/boot-analysis/boot-analysis.c b/utils/boot-analysis/boot-analysis.c
new file mode 100644
index 0000000..b90806b
--- /dev/null
+++ b/utils/boot-analysis/boot-analysis.c
@@ -0,0 +1,1268 @@
+/* libguestfs
+ * Copyright (C) 2016 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/* Trace and analyze the appliance boot process to find out which
+ * steps are taking the most time.  It is not part of the standard
+ * tests.
+ *
+ * This needs to be run on a quiet machine, so that other processes
+ * disturb the timing as little as possible.  The program is
+ * completely safe to run at any time.  It doesn't read or write any
+ * external files, and it doesn't require root.
+ *
+ * You can run it from the build directory like this:
+ *
+ *   make
+ *   ./run utils/boot-analysis/boot-analysis
+ *
+ * The way it works is roughly like this:
+ *
+ * We create a libguestfs handle and register callback handlers so we
+ * can see appliance messages, trace events and so on.
+ *
+ * We then launch the handle and shut it down as quickly as possible.
+ *
+ * While the handle is running, events (seen by the callback handlers)
+ * are written verbatim into an in-memory buffer, with timestamps.
+ *
+ * Afterwards we analyze the result using regular expressions to try
+ * to identify a "timeline" for the handle (eg. at what time did the
+ * BIOS hand control to the kernel).  This analysis is done in
+ * 'boot-analysis-timeline.c'.
+ *
+ * The whole process is repeated across a few runs, and the final
+ * timeline (including statistical analysis of the variation between
+ * runs) gets printed.
+ *
+ * The program is very sensitive to the specific messages printed by
+ * BIOS/kernel/supermin/userspace, so it won't work on non-x86, and it
+ * will require periodic adjustment of the regular expressions in
+ * order to keep things up to date.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <limits.h>
+#include <time.h>
+#include <errno.h>
+#include <error.h>
+#include <ctype.h>
+#include <assert.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <math.h>
+#include <pthread.h>
+
+#include "guestfs.h"
+#include "guestfs-internal-frontend.h"
+
+#include "boot-analysis.h"
+#include "boot-analysis-utils.h"
+
+/* Activities taking longer than this % of the total time, except
+ * those flagged as LONG_ACTIVITY, are highlighted in red.
+ */
+#define WARNING_THRESHOLD 1.0
+
+static const char *append = NULL;
+static int force_colour = 0;
+static int memsize = 0;
+static int smp = 1;
+static int verbose = 0;
+
+static int libvirt_pipe[2] = { -1, -1 };
+static ssize_t libvirt_pass = -1;
+
+/* Because there is a separate thread which collects libvirt log data,
+ * we must protect the pass_data struct with a mutex.  This only
+ * applies during the data collection passes.
+ */
+static pthread_mutex_t pass_data_lock = PTHREAD_MUTEX_INITIALIZER;
+struct pass_data pass_data[NR_TEST_PASSES];
+
+size_t nr_activities;
+struct activity *activities;
+
+static void run_test (void);
+static struct event *add_event (struct pass_data *, uint64_t source);
+static guestfs_h *create_handle (void);
+static void set_up_event_handlers (guestfs_h *g, size_t pass);
+static void libvirt_log_hack (int argc, char **argv);
+static void start_libvirt_thread (size_t pass);
+static void stop_libvirt_thread (void);
+static void add_drive (guestfs_h *g);
+static void check_pass_data (void);
+static void dump_pass_data (void);
+static void analyze_timeline (void);
+static void dump_timeline (void);
+static void print_analysis (void);
+static void print_longest_to_shortest (void);
+static void free_pass_data (void);
+static void free_final_timeline (void);
+static void ansi_green (void);
+static void ansi_red (void);
+static void ansi_blue (void);
+static void ansi_magenta (void);
+static void ansi_restore (void);
+
+static void
+usage (int exitcode)
+{
+  guestfs_h *g;
+  int default_memsize = -1;
+
+  g = guestfs_create ();
+  if (g) {
+    default_memsize = guestfs_get_memsize (g);
+    guestfs_close (g);
+  }
+
+  fprintf (stderr,
+           "boot-analysis: Trace and analyze the appliance boot process.\n"
+           "Usage:\n"
+           "  boot-analysis [--options]\n"
+           "Options:\n"
+           "  --help         Display this usage text and exit.\n"
+           "  --append OPTS  Append OPTS to kernel command line.\n"
+           "  --colour       Output colours, even if not a terminal.\n"
+           "  -m MB\n"
+           "  --memsize MB   Set memory size in MB (default: %d).\n"
+           "  --smp N        Enable N virtual CPUs (default: 1).\n"
+           "  -v|--verbose   Verbose output, useful for debugging.\n",
+           default_memsize);
+  exit (exitcode);
+}
+
+int
+main (int argc, char *argv[])
+{
+  enum { HELP_OPTION = CHAR_MAX + 1 };
+  static const char *options = "m:v";
+  static const struct option long_options[] = {
+    { "help", 0, 0, HELP_OPTION },
+    { "append", 1, 0, 0 },
+    { "color", 0, 0, 0 },
+    { "colour", 0, 0, 0 },
+    { "memsize", 1, 0, 'm' },
+    { "libvirt-pipe-0", 1, 0, 0 }, /* see libvirt_log_hack */
+    { "libvirt-pipe-1", 1, 0, 0 },
+    { "smp", 1, 0, 0 },
+    { "verbose", 0, 0, 'v' },
+    { 0, 0, 0, 0 }
+  };
+  int c, option_index;
+
+  for (;;) {
+    c = getopt_long (argc, argv, options, long_options, &option_index);
+    if (c == -1) break;
+
+    switch (c) {
+    case 0:                     /* Options which are long only. */
+      if (STREQ (long_options[option_index].name, "append")) {
+        append = optarg;
+        break;
+      }
+      else if (STREQ (long_options[option_index].name, "color") ||
+               STREQ (long_options[option_index].name, "colour")) {
+        force_colour = 1;
+        break;
+      }
+      else if (STREQ (long_options[option_index].name, "libvirt-pipe-0")) {
+        if (sscanf (optarg, "%d", &libvirt_pipe[0]) != 1)
+          error (EXIT_FAILURE, 0,
+                 "could not parse libvirt-pipe-0 parameter: %s", optarg);
+        break;
+      }
+      else if (STREQ (long_options[option_index].name, "libvirt-pipe-1")) {
+        if (sscanf (optarg, "%d", &libvirt_pipe[1]) != 1)
+          error (EXIT_FAILURE, 0,
+                 "could not parse libvirt-pipe-1 parameter: %s", optarg);
+        break;
+      }
+      else if (STREQ (long_options[option_index].name, "smp")) {
+        if (sscanf (optarg, "%d", &smp) != 1)
+          error (EXIT_FAILURE, 0,
+                 "could not parse smp parameter: %s", optarg);
+        break;
+      }
+      fprintf (stderr, "%s: unknown long option: %s (%d)\n",
+               guestfs_int_program_name, long_options[option_index].name, option_index);
+      exit (EXIT_FAILURE);
+
+    case 'm':
+      if (sscanf (optarg, "%d", &memsize) != 1) {
+        fprintf (stderr, "%s: could not parse memsize parameter: %s\n",
+                 guestfs_int_program_name, optarg);
+        exit (EXIT_FAILURE);
+      }
+      break;
+
+    case 'v':
+      verbose = 1;
+      break;
+
+    case HELP_OPTION:
+      usage (EXIT_SUCCESS);
+
+    default:
+      usage (EXIT_FAILURE);
+    }
+  }
+
+  libvirt_log_hack (argc, argv);
+
+  if (STRNEQ (host_cpu, "x86_64") && STRNEQ (host_cpu, "aarch64"))
+    fprintf (stderr, "WARNING: host_cpu != x86_64|aarch64: This program may not work or give bogus results.\n");
+
+  run_test ();
+}
+
+static void
+run_test (void)
+{
+  guestfs_h *g;
+  size_t i;
+
+  printf ("Warming up the libguestfs cache ...\n");
+  for (i = 0; i < NR_WARMUP_PASSES; ++i) {
+    g = create_handle ();
+    add_drive (g);
+    if (guestfs_launch (g) == -1)
+      exit (EXIT_FAILURE);
+    guestfs_close (g);
+  }
+
+  printf ("Running the tests in %d passes ...\n", NR_TEST_PASSES);
+  for (i = 0; i < NR_TEST_PASSES; ++i) {
+    g = create_handle ();
+    set_up_event_handlers (g, i);
+    start_libvirt_thread (i);
+    add_drive (g);
+    if (guestfs_launch (g) == -1)
+      exit (EXIT_FAILURE);
+    guestfs_close (g);
+    stop_libvirt_thread ();
+
+    printf ("    pass %zu: %zu events collected in %" PRIi64 " ns\n",
+            i+1, pass_data[i].nr_events, pass_data[i].elapsed_ns);
+  }
+
+  if (verbose)
+    dump_pass_data ();
+
+  printf ("Analyzing the results ...\n");
+  check_pass_data ();
+  construct_timeline ();
+  analyze_timeline ();
+
+  if (verbose)
+    dump_timeline ();
+
+  printf ("\n");
+  g = create_handle ();
+  test_info (g, NR_TEST_PASSES);
+  guestfs_close (g);
+  printf ("\n");
+  print_analysis ();
+  printf ("\n");
+  printf ("Longest activities:\n");
+  printf ("\n");
+  print_longest_to_shortest ();
+
+  free_pass_data ();
+  free_final_timeline ();
+}
+
+static struct event *
+add_event_unlocked (struct pass_data *data, uint64_t source)
+{
+  struct event *ret;
+
+  data->nr_events++;
+  data->events = realloc (data->events,
+                          sizeof (struct event) * data->nr_events);
+  if (data->events == NULL)
+    error (EXIT_FAILURE, errno, "realloc");
+  ret = &data->events[data->nr_events-1];
+  get_time (&ret->t);
+  ret->source = source;
+  ret->message = NULL;
+  return ret;
+}
+
+static struct event *
+add_event (struct pass_data *data, uint64_t source)
+{
+  struct event *ret;
+
+  pthread_mutex_lock (&pass_data_lock);
+  ret = add_event_unlocked (data, source);
+  pthread_mutex_unlock (&pass_data_lock);
+  return ret;
+}
+
+/* Common function to create the handle and set various defaults. */
+static guestfs_h *
+create_handle (void)
+{
+  guestfs_h *g;
+  CLEANUP_FREE char *full_append = NULL;
+
+  g = guestfs_create ();
+  if (!g) error (EXIT_FAILURE, errno, "guestfs_create");
+
+  if (memsize != 0)
+    if (guestfs_set_memsize (g, memsize) == -1)
+      exit (EXIT_FAILURE);
+
+  if (smp >= 2)
+    if (guestfs_set_smp (g, smp) == -1)
+      exit (EXIT_FAILURE);
+
+  /* This changes some details in appliance/init and enables a
+   * detailed trace of calls to initcall functions in the kernel.
+   */
+  if (asprintf (&full_append,
+                "guestfs_boot_analysis=1 "
+                "ignore_loglevel initcall_debug "
+                "%s",
+                append != NULL ? append : "") == -1)
+    error (EXIT_FAILURE, errno, "asprintf");
+
+  if (guestfs_set_append (g, full_append) == -1)
+    exit (EXIT_FAILURE);
+
+  return g;
+}
+
+/* Common function to add the /dev/null drive. */
+static void
+add_drive (guestfs_h *g)
+{
+  if (guestfs_add_drive_opts (g, "/dev/null",
+                              GUESTFS_ADD_DRIVE_OPTS_FORMAT, "raw",
+                              GUESTFS_ADD_DRIVE_OPTS_READONLY, 1,
+                              -1) == -1)
+    exit (EXIT_FAILURE);
+}
+
+/* Called when the handle is closed.  Perform any cleanups required in
+ * the pass_data here.
+ */
+static void
+close_callback (guestfs_h *g, void *datavp, uint64_t source,
+                int eh, int flags,
+                const char *buf, size_t buf_len,
+                const uint64_t *array, size_t array_len)
+{
+  struct pass_data *data = datavp;
+  struct event *event;
+
+  if (!data->seen_launch)
+    return;
+
+  event = add_event (data, source);
+  event->message = strdup ("close callback");
+  if (event->message == NULL)
+    error (EXIT_FAILURE, errno, "strdup");
+
+  get_time (&data->end_t);
+  data->elapsed_ns = timespec_diff (&data->start_t, &data->end_t);
+}
+
+/* Called when the qemu subprocess exits.
+ * XXX This is never called - why?
+ */
+static void
+subprocess_quit_callback (guestfs_h *g, void *datavp, uint64_t source,
+                          int eh, int flags,
+                          const char *buf, size_t buf_len,
+                          const uint64_t *array, size_t array_len)
+{
+  struct pass_data *data = datavp;
+  struct event *event;
+
+  if (!data->seen_launch)
+    return;
+
+  event = add_event (data, source);
+  event->message = strdup ("subprocess quit callback");
+  if (event->message == NULL)
+    error (EXIT_FAILURE, errno, "strdup");
+}
+
+/* Called when the launch operation is complete (the library and the
+ * guestfs daemon and talking to each other).
+ */
+static void
+launch_done_callback (guestfs_h *g, void *datavp, uint64_t source,
+                      int eh, int flags,
+                      const char *buf, size_t buf_len,
+                      const uint64_t *array, size_t array_len)
+{
+  struct pass_data *data = datavp;
+  struct event *event;
+
+  if (!data->seen_launch)
+    return;
+
+  event = add_event (data, source);
+  event->message = strdup ("launch done callback");
+  if (event->message == NULL)
+    error (EXIT_FAILURE, errno, "strdup");
+}
+
+/* Trim \r (multiple) from the end of a string. */
+static void
+trim_r (char *message)
+{
+  size_t len = strlen (message);
+
+  while (len > 0 && message[len-1] == '\r') {
+    message[len-1] = '\0';
+    len--;
+  }
+}
+
+/* Called when we get (possibly part of) a log message (or more than
+ * one log message) from the appliance (which may include qemu, the
+ * BIOS, kernel, etc).
+ */
+static void
+appliance_callback (guestfs_h *g, void *datavp, uint64_t source,
+                    int eh, int flags,
+                    const char *buf, size_t buf_len,
+                    const uint64_t *array, size_t array_len)
+{
+  struct pass_data *data = datavp;
+  struct event *event;
+  size_t i, len, slen;
+
+  if (!data->seen_launch)
+    return;
+
+  /* If the previous log message was incomplete, but time has moved on
+   * a lot, record a new log message anyway, so it gets a new
+   * timestamp.
+   */
+  if (data->incomplete_log_message >= 0) {
+    struct timespec ts;
+    get_time (&ts);
+    if (timespec_diff (&data->events[data->incomplete_log_message].t,
+                       &ts) >= 10000000 /* 10ms */)
+      data->incomplete_log_message = -1;
+  }
+
+  /* If the previous log message was incomplete then we may need to
+   * append part of the current log message to a previous one.
+   */
+  if (data->incomplete_log_message >= 0) {
+    len = buf_len;
+    for (i = 0; i < buf_len; ++i) {
+      if (buf[i] == '\n') {
+        len = i;
+        break;
+      }
+    }
+
+    event = &data->events[data->incomplete_log_message];
+    slen = strlen (event->message);
+    event->message = realloc (event->message, slen + len + 1);
+    if (event->message == NULL)
+      error (EXIT_FAILURE, errno, "realloc");
+    memcpy (event->message + slen, buf, len);
+    event->message[slen + len] = '\0';
+    trim_r (event->message);
+
+    /* Skip what we just added to the previous incomplete message. */
+    buf += len;
+    buf_len -= len;
+
+    if (buf_len == 0)          /* still not complete, more to come! */
+      return;
+
+    /* Skip the \n in the buffer. */
+    buf++;
+    buf_len--;
+    data->incomplete_log_message = -1;
+  }
+
+  /* Add the event, or perhaps multiple events if the message
+   * contains \n characters.
+   */
+  while (buf_len > 0) {
+    len = buf_len;
+    for (i = 0; i < buf_len; ++i) {
+      if (buf[i] == '\n') {
+        len = i;
+        break;
+      }
+    }
+
+    event = add_event (data, source);
+    event->message = strndup (buf, len);
+    if (event->message == NULL)
+      error (EXIT_FAILURE, errno, "strndup");
+    trim_r (event->message);
+
+    /* Skip what we just added to the event. */
+    buf += len;
+    buf_len -= len;
+
+    if (buf_len == 0) {
+      /* Event is incomplete (doesn't end with \n).  We'll finish it
+       * in the next callback.
+       */
+      data->incomplete_log_message = event - data->events;
+      return;
+    }
+
+    /* Skip the \n in the buffer. */
+    buf++;
+    buf_len--;
+  }
+}
+
+/* Called when we get a debug message from the library side.  These
+ * are always delivered as complete messages.
+ */
+static void
+library_callback (guestfs_h *g, void *datavp, uint64_t source,
+                  int eh, int flags,
+                  const char *buf, size_t buf_len,
+                  const uint64_t *array, size_t array_len)
+{
+  struct pass_data *data = datavp;
+  struct event *event;
+
+  if (!data->seen_launch)
+    return;
+
+  event = add_event (data, source);
+  event->message = strndup (buf, buf_len);
+  if (event->message == NULL)
+    error (EXIT_FAILURE, errno, "strndup");
+}
+
+/* Called when we get a call trace message (a libguestfs API function
+ * has been called or is returning).  These are always delivered as
+ * complete messages.
+ */
+static void
+trace_callback (guestfs_h *g, void *datavp, uint64_t source,
+                int eh, int flags,
+                const char *buf, size_t buf_len,
+                const uint64_t *array, size_t array_len)
+{
+  struct pass_data *data = datavp;
+  struct event *event;
+  char *message;
+
+  message = strndup (buf, buf_len);
+  if (message == NULL)
+    error (EXIT_FAILURE, errno, "strndup");
+
+  if (STREQ (message, "launch"))
+    data->seen_launch = 1;
+
+  if (!data->seen_launch) {
+    free (message);
+    return;
+  }
+
+  event = add_event (data, source);
+  event->message = message;
+}
+
+/* Common function to set up event callbacks and record data in memory
+ * for a particular pass (0 <= pass < NR_TEST_PASSES).
+ */
+static void
+set_up_event_handlers (guestfs_h *g, size_t pass)
+{
+  struct pass_data *data;
+
+  assert (/* 0 <= pass && */ pass < NR_TEST_PASSES);
+
+  data = &pass_data[pass];
+  data->pass = pass;
+  data->nr_events = 0;
+  data->events = NULL;
+  get_time (&data->start_t);
+  data->incomplete_log_message = -1;
+  data->seen_launch = 0;
+
+  guestfs_set_event_callback (g, close_callback,
+                              GUESTFS_EVENT_CLOSE, 0, data);
+  guestfs_set_event_callback (g, subprocess_quit_callback,
+                              GUESTFS_EVENT_SUBPROCESS_QUIT, 0, data);
+  guestfs_set_event_callback (g, launch_done_callback,
+                              GUESTFS_EVENT_LAUNCH_DONE, 0, data);
+  guestfs_set_event_callback (g, appliance_callback,
+                              GUESTFS_EVENT_APPLIANCE, 0, data);
+  guestfs_set_event_callback (g, library_callback,
+                              GUESTFS_EVENT_LIBRARY, 0, data);
+  guestfs_set_event_callback (g, trace_callback,
+                              GUESTFS_EVENT_TRACE, 0, data);
+
+  guestfs_set_verbose (g, 1);
+  guestfs_set_trace (g, 1);
+}
+
+/* libvirt debugging sucks in a number of concrete ways:
+ *
+ * - you can't get a synchronous callback from a log message
+ * - you can't enable logging per handle (only globally
+ *   by setting environment variables)
+ * - you can't debug the daemon easily
+ * - it's very complex
+ * - it's very complex but not in ways that are practical or useful
+ *
+ * To get log messages at all, we need to create a pipe connected to a
+ * second thread, and when libvirt prints something to the pipe we log
+ * that.
+ *
+ * However that's not sufficient.  Because logging is only enabled
+ * when libvirt examines environment variables at the start of the
+ * program, we need to create the pipe and then fork+exec a new
+ * instance of the whole program with the pipe and environment
+ * variables set up.
+ */
+static int is_libvirt_backend (guestfs_h *g);
+static void *libvirt_log_thread (void *datavp);
+
+static void
+libvirt_log_hack (int argc, char **argv)
+{
+  guestfs_h *g;
+
+  g = guestfs_create ();
+  if (!is_libvirt_backend (g)) {
+    guestfs_close (g);
+    return;
+  }
+  guestfs_close (g);
+
+  /* Have we set up the pipes and environment and forked yet?  If not,
+   * do that first.
+   */
+  if (libvirt_pipe[0] == -1 || libvirt_pipe[1] == -1) {
+    char log_outputs[64];
+    char **new_argv;
+    char param1[64], param2[64];
+    size_t i;
+    pid_t pid;
+    int status;
+
+    /* Create the pipe.  NB: do NOT use O_CLOEXEC since we want to pass
+     * this pipe into a child process.
+     */
+    if (pipe (libvirt_pipe) == -1)
+      error (EXIT_FAILURE, 0, "pipe2");
+
+    /* Create the environment variables to enable logging in libvirt. */
+    setenv ("LIBVIRT_DEBUG", "1", 1);
+    //setenv ("LIBVIRT_LOG_FILTERS",
+    //        "1:qemu 1:securit 3:file 3:event 3:object 1:util", 1);
+    snprintf (log_outputs, sizeof log_outputs,
+              "1:file:/dev/fd/%d", libvirt_pipe[1]);
+    setenv ("LIBVIRT_LOG_OUTPUTS", log_outputs, 1);
+
+    /* Run self again. */
+    new_argv = malloc ((argc+3) * sizeof (char *));
+    if (new_argv == NULL)
+      error (EXIT_FAILURE, errno, "malloc");
+
+    for (i = 0; i < (size_t) argc; ++i)
+      new_argv[i] = argv[i];
+
+    snprintf (param1, sizeof param1, "--libvirt-pipe-0=%d", libvirt_pipe[0]);
+    new_argv[argc] = param1;
+    snprintf (param2, sizeof param2, "--libvirt-pipe-1=%d", libvirt_pipe[1]);
+    new_argv[argc+1] = param2;
+    new_argv[argc+2] = NULL;
+
+    pid = fork ();
+    if (pid == -1)
+      error (EXIT_FAILURE, errno, "fork");
+    if (pid == 0) {             /* Child process. */
+      execvp (argv[0], new_argv);
+      perror ("execvp");
+      _exit (EXIT_FAILURE);
+    }
+
+    if (waitpid (pid, &status, 0) == -1)
+      error (EXIT_FAILURE, errno, "waitpid");
+    if (WIFEXITED (status))
+      exit (WEXITSTATUS (status));
+    error (EXIT_FAILURE, 0, "unexpected exit status from process: %d", status);
+  }
+
+  /* If we reach this else clause, then we have forked.  Now we must
+   * create a thread to read events from the pipe.  This must be
+   * constantly reading from the pipe, otherwise we will deadlock.
+   * During the warm-up phase we end up throwing away messages.
+   */
+  else {
+    pthread_t thread;
+    pthread_attr_t attr;
+    int r;
+
+    r = pthread_attr_init (&attr);
+    if (r != 0)
+      error (EXIT_FAILURE, r, "pthread_attr_init");
+    r = pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
+    if (r != 0)
+      error (EXIT_FAILURE, r, "pthread_attr_setdetachstate");
+    r = pthread_create (&thread, &attr, libvirt_log_thread, NULL);
+    if (r != 0)
+      error (EXIT_FAILURE, r, "pthread_create");
+    pthread_attr_destroy (&attr);
+  }
+}
+
+static void
+start_libvirt_thread (size_t pass)
+{
+  /* In the non-libvirt case, this variable is ignored. */
+  pthread_mutex_lock (&pass_data_lock);
+  libvirt_pass = pass;
+  pthread_mutex_unlock (&pass_data_lock);
+}
+
+static void
+stop_libvirt_thread (void)
+{
+  /* In the non-libvirt case, this variable is ignored. */
+  pthread_mutex_lock (&pass_data_lock);
+  libvirt_pass = -1;
+  pthread_mutex_unlock (&pass_data_lock);
+}
+
+/* The separate "libvirt thread".  It loops reading debug messages
+ * printed by libvirt and adds them to the pass_data.
+ */
+static void *
+libvirt_log_thread (void *arg)
+{
+  struct event *event;
+  CLEANUP_FREE char *buf = NULL;
+  ssize_t r;
+
+  buf = malloc (BUFSIZ);
+  if (buf == NULL)
+    error (EXIT_FAILURE, errno, "malloc");
+
+  while ((r = read (libvirt_pipe[0], buf, BUFSIZ)) > 0) {
+    pthread_mutex_lock (&pass_data_lock);
+    if (libvirt_pass == -1) goto discard;
+    event =
+      add_event_unlocked (&pass_data[libvirt_pass], SOURCE_LIBVIRT);
+    event->message = strndup (buf, r);
+    if (event->message == NULL)
+      error (EXIT_FAILURE, errno, "strndup");
+  discard:
+    pthread_mutex_unlock (&pass_data_lock);
+  }
+
+  if (r == -1)
+    error (EXIT_FAILURE, errno, "libvirt_log_thread: read");
+
+  /* It's possible for the pipe to be closed (r == 0) if thread
+   * cancellation is delayed after the main thread exits, so just
+   * ignore that case and exit.
+   */
+  pthread_exit (NULL);
+}
+
+static int
+is_libvirt_backend (guestfs_h *g)
+{
+  CLEANUP_FREE char *backend = guestfs_get_backend (g);
+
+  return backend &&
+    (STREQ (backend, "libvirt") || STRPREFIX (backend, "libvirt:"));
+}
+
+/* Sanity check the collected events. */
+static void
+check_pass_data (void)
+{
+  size_t i, j, len;
+  int64_t ns;
+  const char *message;
+
+  for (i = 0; i < NR_TEST_PASSES; ++i) {
+    assert (pass_data[i].pass == i);
+    assert (pass_data[i].elapsed_ns > 1000);
+    assert (pass_data[i].nr_events > 0);
+    assert (pass_data[i].events != NULL);
+
+    for (j = 0; j < pass_data[i].nr_events; ++j) {
+      assert (pass_data[i].events[j].t.tv_sec > 0);
+      if (j > 0) {
+        ns = timespec_diff (&pass_data[i].events[j-1].t,
+                            &pass_data[i].events[j].t);
+        assert (ns >= 0);
+      }
+      assert (pass_data[i].events[j].source != 0);
+      message = pass_data[i].events[j].message;
+      assert (message != NULL);
+      assert (pass_data[i].events[j].source != GUESTFS_EVENT_APPLIANCE ||
+              strchr (message, '\n') == NULL);
+      len = strlen (message);
+      assert (len == 0 || message[len-1] != '\r');
+    }
+  }
+}
+
+static void
+print_escaped_string (const char *message)
+{
+  while (*message) {
+    if (isprint (*message))
+      putchar (*message);
+    else
+      printf ("\\x%02x", (unsigned int) *message);
+    message++;
+  }
+}
+
+/* Dump the events to stdout, if verbose is set. */
+static void
+dump_pass_data (void)
+{
+  size_t i, j;
+
+  for (i = 0; i < NR_TEST_PASSES; ++i) {
+    printf ("pass %zu\n", pass_data[i].pass);
+    printf ("    number of events collected %zu\n", pass_data[i].nr_events);
+    printf ("    elapsed time %" PRIi64 " ns\n", pass_data[i].elapsed_ns);
+    for (j = 0; j < pass_data[i].nr_events; ++j) {
+      int64_t ns, diff_ns;
+      CLEANUP_FREE char *source_str = NULL;
+
+      ns = timespec_diff (&pass_data[i].start_t, &pass_data[i].events[j].t);
+      source_str = source_to_string (pass_data[i].events[j].source);
+      printf ("    %.1fms ", ns / 1000000.0);
+      if (j > 0) {
+	diff_ns = timespec_diff (&pass_data[i].events[j-1].t,
+				 &pass_data[i].events[j].t);
+	printf ("(+%.1f) ", diff_ns / 1000000.0);
+      }
+      printf ("[%s] \"", source_str);
+      print_escaped_string (pass_data[i].events[j].message);
+      printf ("\"\n");
+    }
+  }
+}
+
+/* Convert source to a printable string.  The caller must free the
+ * returned string.
+ */
+char *
+source_to_string (uint64_t source)
+{
+  char *ret;
+
+  if (source == SOURCE_LIBVIRT) {
+    ret = strdup ("libvirt");
+    if (ret == NULL)
+      error (EXIT_FAILURE, errno, "strdup");
+  }
+  else
+    ret = guestfs_event_to_string (source);
+
+  return ret;                   /* caller frees */
+}
+
+int
+activity_exists (const char *name)
+{
+  size_t i;
+
+  for (i = 0; i < nr_activities; ++i)
+    if (STREQ (activities[i].name, name))
+      return 1;
+  return 0;
+}
+
+/* Add an activity to the global list. */
+struct activity *
+add_activity (const char *name, int flags)
+{
+  struct activity *ret;
+  size_t i;
+
+  /* You shouldn't have two activities with the same name. */
+  assert (!activity_exists (name));
+
+  nr_activities++;
+  activities = realloc (activities, sizeof (struct activity) * nr_activities);
+  if (activities == NULL)
+    error (EXIT_FAILURE, errno, "realloc");
+  ret = &activities[nr_activities-1];
+  ret->name = strdup (name);
+  if (ret->name == NULL)
+    error (EXIT_FAILURE, errno, "strdup");
+  ret->flags = flags;
+
+  for (i = 0; i < NR_TEST_PASSES; ++i)
+    ret->start_event[i] = ret->end_event[i] = 0;
+
+  return ret;
+}
+
+struct activity *
+find_activity (const char *name)
+{
+  size_t i;
+
+  for (i = 0; i < nr_activities; ++i)
+    if (STREQ (activities[i].name, name))
+      return &activities[i];
+  error (EXIT_FAILURE, 0,
+         "internal error: could not find activity '%s'", name);
+  /*NOTREACHED*/
+  abort ();
+}
+
+int
+activity_exists_with_no_data (const char *name, size_t pass)
+{
+  size_t i;
+
+  for (i = 0; i < nr_activities; ++i)
+    if (STREQ (activities[i].name, name) &&
+        activities[i].start_event[pass] == 0 &&
+        activities[i].end_event[pass] == 0)
+      return 1;
+  return 0;
+}
+
+static int
+compare_activities_by_t (const void *av, const void *bv)
+{
+  const struct activity *a = av;
+  const struct activity *b = bv;
+
+  return a->t - b->t;
+}
+
+/* Go through the activities, computing the start and elapsed time. */
+static void
+analyze_timeline (void)
+{
+  struct activity *activity;
+  size_t i, j;
+  int64_t delta_ns;
+
+  for (j = 0; j < nr_activities; ++j) {
+    activity = &activities[j];
+
+    activity->t = 0;
+    activity->mean = 0;
+    for (i = 0; i < NR_TEST_PASSES; ++i) {
+      delta_ns =
+        timespec_diff (&pass_data[i].events[0].t,
+                       &pass_data[i].events[activity->start_event[i]].t);
+      activity->t += delta_ns;
+
+      delta_ns =
+        timespec_diff (&pass_data[i].events[activity->start_event[i]].t,
+                       &pass_data[i].events[activity->end_event[i]].t);
+      activity->mean += delta_ns;
+    }
+
+    /* Divide through to get real start time and mean of each activity. */
+    activity->t /= NR_TEST_PASSES;
+    activity->mean /= NR_TEST_PASSES;
+
+    /* Calculate the end time of this activity.  It's convenient when
+     * drawing the timeline for one activity to finish just before the
+     * next activity starts, rather than having them end and start at
+     * the same time, hence ``- 1'' here.
+     */
+    activity->end_t = activity->t + activity->mean - 1;
+
+    /* The above only calculated mean.  Now we are able to
+     * calculate from the mean the variance and the standard
+     * deviation.
+     */
+    activity->variance = 0;
+    for (i = 0; i < NR_TEST_PASSES; ++i) {
+      delta_ns =
+        timespec_diff (&pass_data[i].events[activity->start_event[i]].t,
+                       &pass_data[i].events[activity->end_event[i]].t);
+      activity->variance += pow (delta_ns - activity->mean, 2);
+    }
+    activity->variance /= NR_TEST_PASSES;
+
+    activity->sd = sqrt (activity->variance);
+  }
+
+  /* Get the total mean elapsed time from the special "run" activity. */
+  activity = find_activity ("run");
+  for (j = 0; j < nr_activities; ++j) {
+    activities[j].percent = 100.0 * activities[j].mean / activity->mean;
+
+    activities[j].warning =
+      !(activities[j].flags & LONG_ACTIVITY) &&
+      activities[j].percent >= WARNING_THRESHOLD;
+  }
+
+  /* Sort the activities by start time. */
+  qsort (activities, nr_activities, sizeof (struct activity),
+         compare_activities_by_t);
+}
+
+/* Dump the timeline to stdout, if verbose is set. */
+static void
+dump_timeline (void)
+{
+  size_t i;
+
+  for (i = 0; i < nr_activities; ++i) {
+    printf ("activity %zu:\n", i);
+    printf ("    name = %s\n", activities[i].name);
+    printf ("    start - end = %.1f - %.1f\n",
+            activities[i].t, activities[i].end_t);
+    printf ("    mean elapsed = %.1f\n", activities[i].mean);
+    printf ("    variance = %.1f\n", activities[i].variance);
+    printf ("    s.d = %.1f\n", activities[i].sd);
+    printf ("    percent = %.1f\n", activities[i].percent);
+  }
+}
+
+static void
+print_activity (struct activity *activity)
+{
+  if (activity->warning) ansi_red (); else ansi_green ();
+  print_escaped_string (activity->name);
+  ansi_restore ();
+  printf (" %.1fms ±%.1fms ",
+          activity->mean / 1000000, activity->sd / 1000000);
+  if (activity->warning) ansi_red (); else ansi_green ();
+  printf ("(%.1f%%) ", activity->percent);
+  ansi_restore ();
+}
+
+static void
+print_analysis (void)
+{
+  double t = -1;                /* Current time. */
+  /* Which columns contain activities that we are displaying now?
+   * -1 == unused column, else index of an activity
+   */
+  CLEANUP_FREE ssize_t *columns = NULL;
+  const size_t nr_columns = nr_activities;
+  size_t last_free_column = 0;
+
+  size_t i, j;
+  double last_t, smallest_next_t;
+  const double MAX_T = 1e20;
+
+  columns = malloc (nr_columns * sizeof (ssize_t));
+  if (columns == NULL) error (EXIT_FAILURE, errno, "malloc");
+  for (j = 0; j < nr_columns; ++j)
+    columns[j] = -1;
+
+  for (;;) {
+    /* Find the next significant time to display, which is a time when
+     * some activity started or ended.
+     */
+    smallest_next_t = MAX_T;
+    for (i = 0; i < nr_activities; ++i) {
+      if (t < activities[i].t && activities[i].t < smallest_next_t)
+        smallest_next_t = activities[i].t;
+      else if (t < activities[i].end_t && activities[i].end_t < smallest_next_t)
+        smallest_next_t = activities[i].end_t;
+    }
+    if (smallest_next_t == MAX_T)
+      break;                    /* Finished. */
+
+    last_t = t;
+    t = smallest_next_t;
+
+    /* Draw a spacer line, but only if last_t -> t is a large jump. */
+    if (t - last_t >= 1000000 /* ns */) {
+      printf ("          ");
+      ansi_magenta ();
+      for (j = 0; j < last_free_column; ++j) {
+        if (columns[j] >= 0 &&
+            activities[columns[j]].end_t != last_t /* !▼ */)
+          printf ("│ ");
+        else
+          printf ("  ");
+      }
+      ansi_restore ();
+      printf ("\n");
+    }
+
+    /* If there are any activities that ended before this time, drop
+     * them from the columns list.
+     */
+    for (i = 0; i < nr_activities; ++i) {
+      if (activities[i].end_t < t) {
+        for (j = 0; j < nr_columns; ++j)
+          if (columns[j] == (ssize_t) i) {
+            columns[j] = -1;
+            break;
+          }
+      }
+    }
+
+    /* May need to adjust last_free_column after previous operation. */
+    while (last_free_column > 0 && columns[last_free_column-1] == -1)
+      last_free_column--;
+
+    /* If there are any activities starting at this time, add them to
+     * the right hand end of the columns list.
+     */
+    for (i = 0; i < nr_activities; ++i) {
+      if (activities[i].t == t)
+        columns[last_free_column++] = i;
+    }
+
+    /* Draw the line. */
+    ansi_blue ();
+    printf ("%6.1fms: ", t / 1000000);
+
+    ansi_magenta ();
+    for (j = 0; j < last_free_column; ++j) {
+      if (columns[j] >= 0) {
+        if (activities[columns[j]].t == t)
+          printf ("▲ ");
+        else if (activities[columns[j]].end_t == t)
+          printf ("▼ ");
+        else
+          printf ("│ ");
+      }
+      else
+        printf ("  ");
+    }
+    ansi_restore ();
+
+    for (j = 0; j < last_free_column; ++j) {
+      if (columns[j] >= 0 && activities[columns[j]].t == t) /* ▲ */
+        print_activity (&activities[columns[j]]);
+    }
+
+    printf ("\n");
+  }
+}
+
+static int
+compare_activities_pointers_by_mean (const void *av, const void *bv)
+{
+  const struct activity * const *a = av;
+  const struct activity * const *b = bv;
+
+  return (*b)->mean - (*a)->mean;
+}
+
+static void
+print_longest_to_shortest (void)
+{
+  size_t i;
+  CLEANUP_FREE struct activity **longest;
+
+  /* Sort the activities longest first.  In order not to affect the
+   * global activities array, sort an array of pointers to the
+   * activities instead.
+   */
+  longest = malloc (sizeof (struct activity *) * nr_activities);
+  for (i = 0; i < nr_activities; ++i)
+    longest[i] = &activities[i];
+
+  qsort (longest, nr_activities, sizeof (struct activity *),
+         compare_activities_pointers_by_mean);
+
+  /* Display the activities, longest first. */
+  for (i = 0; i < nr_activities; ++i) {
+    print_activity (longest[i]);
+    printf ("\n");
+  }
+}
+
+/* Free the non-static part of the pass_data structures. */
+static void
+free_pass_data (void)
+{
+  size_t i, j;
+
+  for (i = 0; i < NR_TEST_PASSES; ++i) {
+    for (j = 0; j < pass_data[i].nr_events; ++j)
+      free (pass_data[i].events[j].message);
+    free (pass_data[i].events);
+  }
+}
+
+static void
+free_final_timeline (void)
+{
+  size_t i;
+
+  for (i = 0; i < nr_activities; ++i)
+    free (activities[i].name);
+  free (activities);
+}
+
+/* Colours. */
+static void
+ansi_green (void)
+{
+  if (force_colour || isatty (1))
+    fputs ("\033[0;32m", stdout);
+}
+
+static void
+ansi_red (void)
+{
+  if (force_colour || isatty (1))
+    fputs ("\033[1;31m", stdout);
+}
+
+static void
+ansi_blue (void)
+{
+  if (force_colour || isatty (1))
+    fputs ("\033[1;34m", stdout);
+}
+
+static void
+ansi_magenta (void)
+{
+  if (force_colour || isatty (1))
+    fputs ("\033[1;35m", stdout);
+}
+
+static void
+ansi_restore (void)
+{
+  if (force_colour || isatty (1))
+    fputs ("\033[0m", stdout);
+}
diff --git a/utils/boot-analysis/boot-analysis.h b/utils/boot-analysis/boot-analysis.h
new file mode 100644
index 0000000..a07f12e
--- /dev/null
+++ b/utils/boot-analysis/boot-analysis.h
@@ -0,0 +1,102 @@
+/* libguestfs
+ * Copyright (C) 2016 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef GUESTFS_BOOT_ANALYSIS_H_
+#define GUESTFS_BOOT_ANALYSIS_H_
+
+#define NR_WARMUP_PASSES 3
+#define NR_TEST_PASSES   5
+
+/* Per-pass data collected. */
+struct pass_data {
+  size_t pass;
+  struct timespec start_t;
+  struct timespec end_t;
+  int64_t elapsed_ns;
+
+  /* Array of timestamped events. */
+  size_t nr_events;
+  struct event *events;
+
+  /* Was the previous appliance log message incomplete?  If so, this
+   * contains the index of that incomplete message in the events
+   * array.
+   */
+  ssize_t incomplete_log_message;
+
+  /* Have we seen the launch event yet?  We don't record events until
+   * this one has been received.  This makes it easy to base the
+   * timeline at event 0.
+   */
+  int seen_launch;
+};
+
+/* The 'source' field in the event is a guestfs event
+ * (GUESTFS_EVENT_*).  We also wish to encode libvirt as a source, so
+ * we use a magic/impossible value for that here.  Note that events
+ * are bitmasks, and normally no more than one bit may be set.
+ */
+#define SOURCE_LIBVIRT ((uint64_t)~0)
+extern char *source_to_string (uint64_t source);
+
+struct event {
+  struct timespec t;
+  uint64_t source;
+  char *message;
+};
+
+extern struct pass_data pass_data[NR_TEST_PASSES];
+
+/* The final timeline consisting of various activities starting and
+ * ending.  We're interested in when the activities start, and how
+ * long they take (mean, variance, standard deviation of length).
+ */
+struct activity {
+  char *name;                   /* Name of this activity. */
+  int flags;
+#define LONG_ACTIVITY 1         /* Expected to take a long time. */
+
+  /* For each pass, record the actual start & end events of this
+   * activity.
+   */
+  size_t start_event[NR_TEST_PASSES];
+  size_t end_event[NR_TEST_PASSES];
+
+  double t;                     /* Start (ns offset). */
+  double end_t;                 /* t + mean - 1 */
+
+  /* Length of this activity. */
+  double mean;                  /* Mean time elapsed (ns). */
+  double variance;              /* Variance. */
+  double sd;                    /* Standard deviation. */
+  double percent;               /* Percent of total elapsed time. */
+
+  int warning;                  /* Appears in red. */
+};
+
+extern size_t nr_activities;
+extern struct activity *activities;
+
+extern int activity_exists (const char *name);
+extern struct activity *add_activity (const char *name, int flags);
+extern struct activity *find_activity (const char *name);
+extern int activity_exists_with_no_data (const char *name, size_t pass);
+
+extern void construct_timeline (void);
+
+#endif /* GUESTFS_BOOT_ANALYSIS_H_ */
diff --git a/utils/boot-benchmark/Makefile.am b/utils/boot-benchmark/Makefile.am
new file mode 100644
index 0000000..429832a
--- /dev/null
+++ b/utils/boot-benchmark/Makefile.am
@@ -0,0 +1,44 @@
+# libguestfs
+# Copyright (C) 2011-2016 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# Safety and liveness tests of components that libguestfs depends upon
+# (not of libguestfs itself).  Mainly this is for qemu and the kernel.
+# This test is the first to run.
+
+include $(top_srcdir)/subdir-rules.mk
+
+noinst_PROGRAMS = boot-benchmark
+
+boot_benchmark_SOURCES = \
+	boot-benchmark.c \
+	../boot-analysis/boot-analysis-utils.c \
+	../boot-analysis/boot-analysis-utils.h
+boot_benchmark_CPPFLAGS = \
+	-I$(top_srcdir)/gnulib/lib -I$(top_builddir)/gnulib/lib \
+	-I$(top_srcdir)/src -I$(top_builddir)/src \
+	-I$(top_srcdir)/utils/boot-analysis
+boot_benchmark_CFLAGS = \
+	$(WARN_CFLAGS) $(WERROR_CFLAGS)
+boot_benchmark_LDADD = \
+	$(top_builddir)/src/libutils.la \
+	$(top_builddir)/src/libguestfs.la \
+	$(LIBXML2_LIBS) \
+	$(LTLIBINTL) \
+	$(top_builddir)/gnulib/lib/libgnu.la \
+	-lm
+
+EXTRA_DIST = boot-benchmark-range.pl
diff --git a/utils/boot-benchmark/boot-benchmark-range.pl b/utils/boot-benchmark/boot-benchmark-range.pl
new file mode 100755
index 0000000..b69a835
--- /dev/null
+++ b/utils/boot-benchmark/boot-benchmark-range.pl
@@ -0,0 +1,240 @@
+#!/usr/bin/env perl
+# Copyright (C) 2016 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use warnings;
+use strict;
+
+use Pod::Usage;
+use Getopt::Long;
+
+=head1 NAME
+
+boot-benchmark-range.pl - Benchmark libguestfs across a range of commits
+from another project
+
+=head1 SYNOPSIS
+
+ LIBGUESTFS_BACKEND=direct \
+ LIBGUESTFS_HV=/path/to/qemu/x86_64-softmmu/qemu-system-x86_64 \
+   ./run \
+   utils/boot-benchmark/boot-benchmark-range.pl /path/to/qemu HEAD~50..HEAD
+
+=head1
+
+Run F<utils/boot-benchmark/boot-benchmark> across a range of commits
+in another project.  This is useful for finding performance
+regressions in other programs such as qemu or the Linux kernel which
+might be affecting libguestfs.
+
+For example, suppose you suspect there has been a performance
+regression in qemu, somewhere between C<HEAD~50..HEAD>.  You could run
+the script like this:
+
+ LIBGUESTFS_BACKEND=direct \
+ LIBGUESTFS_HV=/path/to/qemu/x86_64-softmmu/qemu-system-x86_64 \
+   ./run \
+   utils/boot-benchmark/boot-benchmark-range.pl /path/to/qemu HEAD~50..HEAD
+
+where F</path/to/qemu> is the path to the qemu git repository.
+
+The output is a list of the qemu commits, annotated by the benchmark
+time and some other information about how the time compares to the
+previous commit.
+
+You should run these tests on an unloaded machine.  In particular
+running a desktop environment, web browser and so on can make the
+benchmarks useless.
+
+=head1 OPTIONS
+
+=over 4
+
+=cut
+
+my $help;
+
+=item B<--help>
+
+Display brief help.
+
+=cut
+
+my $man;
+
+=item B<--man>
+
+Display full documentation (man page).
+
+=cut
+
+my $benchmark_command;
+
+=item B<--benchmark> C<boot-benchmark>
+
+Set the name of the benchmark to run.  You only need to use this if
+the script cannot find the right path to the libguestfs
+F<utils/boot-benchmark/boot-benchmark> program.  By default the script
+looks for this file in the same directory as its executable.
+
+=cut
+
+my $make_command = "make";
+
+=item B<--make> C<make>
+
+Set the command used to build the other project.  The default is
+to run C<make>.
+
+If the command fails, then the commit is skipped.
+
+=back
+
+=cut
+
+# Clean up the program name.
+my $progname = $0;
+$progname =~ s{.*/}{};
+
+# Parse options.
+GetOptions ("help|?" => \$help,
+            "man" => \$man,
+            "benchmark=s" => \$benchmark_command,
+            "make=s" => \$make_command,
+    ) or pod2usage (2);
+pod2usage (-exitval => 0) if $help;
+pod2usage (-exitval => 0, -verbose => 2) if $man;
+
+die "$progname: missing argument: requires path to git repository and range of commits\n" unless @ARGV == 2;
+
+my $dir = $ARGV[0];
+my $range = $ARGV[1];
+
+die "$progname: $dir is not a git repository\n"
+    unless -d $dir && -d "$dir/.git";
+
+sub silently_run
+{
+    open my $saveout, ">&STDOUT";
+    open my $saveerr, ">&STDERR";
+    open STDOUT, ">/dev/null";
+    open STDERR, ">/dev/null";
+    my $ret = system (@_);
+    open STDOUT, ">&", $saveout;
+    open STDERR, ">&", $saveerr;
+    return $ret;
+}
+
+# Find the benchmark program and check it works.
+unless (defined $benchmark_command) {
+    $benchmark_command = $0;
+    $benchmark_command =~ s{/[^/]+$}{};
+    $benchmark_command .= "/boot-benchmark";
+
+    my $r = silently_run ("$benchmark_command", "--help");
+    die "$progname: cannot locate boot-benchmark program, try using --benchmark\n" unless $r == 0;
+}
+
+# Get the top-most commit from the remote, and restore it on exit.
+my $top_commit = `git -C '$dir' rev-parse HEAD`;
+chomp $top_commit;
+
+sub checkout
+{
+    my $sha = shift;
+    my $ret = silently_run ("git", "-C", $dir, "checkout", $sha);
+    return $ret;
+}
+
+END {
+    checkout ($top_commit);
+}
+
+# Get the range of commits and log messages.
+my @range = ();
+open RANGE, "git -C '$dir' log --reverse --oneline $range |" or die;
+while (<RANGE>) {
+    if (m/^([0-9a-f]+) (.*)/) {
+        my $sha = $1;
+        my $msg = $2;
+        push @range, [ $sha, $msg ];
+    }
+}
+close RANGE or die;
+
+# Run the test.
+my $prev_ms;
+foreach (@range) {
+    my ($sha, $msg) = @$_;
+    my $r;
+
+    print "\n";
+    print "$sha $msg\n";
+
+    # Checkout this commit in the other repo.
+    $r = checkout ($sha);
+    if ($r != 0) {
+        print "git checkout failed\n";
+        next;
+    }
+
+    # Build the repo, silently.
+    $r = silently_run ("cd $dir && $make_command");
+    if ($r != 0) {
+        print "build failed\n";
+        next;
+    }
+
+    # Run the benchmark program and get the timing.
+    my ($time_ms, $time_str);
+    open BENCHMARK, "$benchmark_command | grep '^Result:' |" or die;
+    while (<BENCHMARK>) {
+        die unless m/^Result: (([\d.]+)ms ±[\d.]+ms)/;
+        $time_ms = $2;
+        $time_str = $1;
+    }
+    close BENCHMARK;
+
+    print "\t", $time_str;
+    if (defined $prev_ms) {
+        if ($prev_ms > $time_ms) {
+            my $pc = 100 * ($prev_ms-$time_ms) / $time_ms;
+            if ($pc >= 1) {
+                printf (" ↑ improves performance by %0.1f%%", $pc);
+            }
+        } elsif ($prev_ms < $time_ms) {
+            my $pc = 100 * ($time_ms-$prev_ms) / $prev_ms;
+            if ($pc >= 1) {
+                printf (" ↓ degrades performance by %0.1f%%", $pc);
+            }
+        }
+    }
+    print "\n";
+    $prev_ms = $time_ms;
+}
+
+=head1 SEE ALSO
+
+L<git(1)>,
+L<guestfs-performance(1)>.
+
+=head1 AUTHOR
+
+Richard W.M. Jones.
+
+=head1 COPYRIGHT
+
+Copyright (C) 2016 Red Hat Inc.
diff --git a/utils/boot-benchmark/boot-benchmark.c b/utils/boot-benchmark/boot-benchmark.c
new file mode 100644
index 0000000..0508ee9
--- /dev/null
+++ b/utils/boot-benchmark/boot-benchmark.c
@@ -0,0 +1,225 @@
+/* libguestfs
+ * Copyright (C) 2016 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/* Benchmark the time taken to boot the libguestfs appliance. */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <string.h>
+#include <getopt.h>
+#include <limits.h>
+#include <time.h>
+#include <errno.h>
+#include <error.h>
+#include <assert.h>
+#include <math.h>
+
+#include "guestfs.h"
+#include "guestfs-internal-frontend.h"
+
+#include "boot-analysis-utils.h"
+
+#define NR_WARMUP_PASSES 3
+#define NR_TEST_PASSES   10
+
+static const char *append = NULL;
+static int memsize = 0;
+static int smp = 1;
+
+static void run_test (void);
+static guestfs_h *create_handle (void);
+static void add_drive (guestfs_h *g);
+
+static void
+usage (int exitcode)
+{
+  guestfs_h *g;
+  int default_memsize = -1;
+
+  g = guestfs_create ();
+  if (g) {
+    default_memsize = guestfs_get_memsize (g);
+    guestfs_close (g);
+  }
+
+  fprintf (stderr,
+           "boot-benchmark: Benchmark the time taken to boot the libguestfs appliance.\n"
+           "Usage:\n"
+           "  boot-benchmark [--options]\n"
+           "Options:\n"
+           "  --help         Display this usage text and exit.\n"
+           "  --append OPTS  Append OPTS to kernel command line.\n"
+           "  -m MB\n"
+           "  --memsize MB   Set memory size in MB (default: %d).\n"
+           "  --smp N        Enable N virtual CPUs (default: 1).\n",
+           default_memsize);
+  exit (exitcode);
+}
+
+int
+main (int argc, char *argv[])
+{
+  enum { HELP_OPTION = CHAR_MAX + 1 };
+  static const char *options = "m:";
+  static const struct option long_options[] = {
+    { "help", 0, 0, HELP_OPTION },
+    { "append", 1, 0, 0 },
+    { "memsize", 1, 0, 'm' },
+    { "smp", 1, 0, 0 },
+    { 0, 0, 0, 0 }
+  };
+  int c, option_index;
+
+  for (;;) {
+    c = getopt_long (argc, argv, options, long_options, &option_index);
+    if (c == -1) break;
+
+    switch (c) {
+    case 0:                     /* Options which are long only. */
+      if (STREQ (long_options[option_index].name, "append")) {
+        append = optarg;
+        break;
+      }
+      else if (STREQ (long_options[option_index].name, "smp")) {
+        if (sscanf (optarg, "%d", &smp) != 1) {
+          fprintf (stderr, "%s: could not parse smp parameter: %s\n",
+                   guestfs_int_program_name, optarg);
+          exit (EXIT_FAILURE);
+        }
+        break;
+      }
+      fprintf (stderr, "%s: unknown long option: %s (%d)\n",
+               guestfs_int_program_name, long_options[option_index].name, option_index);
+      exit (EXIT_FAILURE);
+
+    case 'm':
+      if (sscanf (optarg, "%d", &memsize) != 1) {
+        fprintf (stderr, "%s: could not parse memsize parameter: %s\n",
+                 guestfs_int_program_name, optarg);
+        exit (EXIT_FAILURE);
+      }
+      break;
+
+    case HELP_OPTION:
+      usage (EXIT_SUCCESS);
+
+    default:
+      usage (EXIT_FAILURE);
+    }
+  }
+
+  run_test ();
+}
+
+static void
+run_test (void)
+{
+  guestfs_h *g;
+  size_t i;
+  int64_t ns[NR_TEST_PASSES];
+  double mean;
+  double variance;
+  double sd;
+
+  printf ("Warming up the libguestfs cache ...\n");
+  for (i = 0; i < NR_WARMUP_PASSES; ++i) {
+    g = create_handle ();
+    add_drive (g);
+    if (guestfs_launch (g) == -1)
+      exit (EXIT_FAILURE);
+    guestfs_close (g);
+  }
+
+  printf ("Running the tests ...\n");
+  for (i = 0; i < NR_TEST_PASSES; ++i) {
+    struct timespec start_t, end_t;
+
+    g = create_handle ();
+    add_drive (g);
+    get_time (&start_t);
+    if (guestfs_launch (g) == -1)
+      exit (EXIT_FAILURE);
+    guestfs_close (g);
+    get_time (&end_t);
+
+    ns[i] = timespec_diff (&start_t, &end_t);
+  }
+
+  /* Calculate the mean. */
+  mean = 0;
+  for (i = 0; i < NR_TEST_PASSES; ++i)
+    mean += ns[i];
+  mean /= NR_TEST_PASSES;
+
+  /* Calculate the variance and standard deviation. */
+  variance = 0;
+  for (i = 0; i < NR_TEST_PASSES; ++i)
+    variance = pow (ns[i] - mean, 2);
+  variance /= NR_TEST_PASSES;
+  sd = sqrt (variance);
+
+  /* Print the test parameters. */
+  printf ("\n");
+  g = create_handle ();
+  test_info (g, NR_TEST_PASSES);
+  guestfs_close (g);
+
+  /* Print the result. */
+  printf ("\n");
+  printf ("Result: %.1fms ±%.1fms\n", mean / 1000000, sd / 1000000);
+}
+
+/* Common function to create the handle and set various defaults. */
+static guestfs_h *
+create_handle (void)
+{
+  guestfs_h *g;
+  CLEANUP_FREE char *full_append = NULL;
+
+  g = guestfs_create ();
+  if (!g) error (EXIT_FAILURE, errno, "guestfs_create");
+
+  if (memsize != 0)
+    if (guestfs_set_memsize (g, memsize) == -1)
+      exit (EXIT_FAILURE);
+
+  if (smp >= 2)
+    if (guestfs_set_smp (g, smp) == -1)
+      exit (EXIT_FAILURE);
+
+  if (append != NULL)
+    if (guestfs_set_append (g, full_append) == -1)
+      exit (EXIT_FAILURE);
+
+  return g;
+}
+
+/* Common function to add the /dev/null drive. */
+static void
+add_drive (guestfs_h *g)
+{
+  if (guestfs_add_drive_opts (g, "/dev/null",
+                              GUESTFS_ADD_DRIVE_OPTS_FORMAT, "raw",
+                              GUESTFS_ADD_DRIVE_OPTS_READONLY, 1,
+                              -1) == -1)
+    exit (EXIT_FAILURE);
+}
diff --git a/utils/qemu-boot/Makefile.am b/utils/qemu-boot/Makefile.am
new file mode 100644
index 0000000..d4716cc
--- /dev/null
+++ b/utils/qemu-boot/Makefile.am
@@ -0,0 +1,39 @@
+# libguestfs
+# Copyright (C) 2011-2016 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+include $(top_srcdir)/subdir-rules.mk
+
+noinst_PROGRAMS = qemu-boot
+
+qemu_boot_SOURCES = \
+	../../df/estimate-max-threads.c \
+	../../df/estimate-max-threads.h \
+	qemu-boot.c
+qemu_boot_CPPFLAGS = \
+	-I$(top_srcdir)/gnulib/lib -I$(top_builddir)/gnulib/lib \
+	-I$(top_srcdir)/src -I$(top_builddir)/src \
+	-I$(top_srcdir)/df
+qemu_boot_CFLAGS = \
+	-pthread \
+	$(WARN_CFLAGS) $(WERROR_CFLAGS)
+qemu_boot_LDADD = \
+	$(top_builddir)/src/libutils.la \
+	$(top_builddir)/src/libguestfs.la \
+	$(LIBXML2_LIBS) \
+	$(LIBVIRT_LIBS) \
+	$(LTLIBINTL) \
+	$(top_builddir)/gnulib/lib/libgnu.la
diff --git a/utils/qemu-boot/qemu-boot.c b/utils/qemu-boot/qemu-boot.c
new file mode 100644
index 0000000..336c26e
--- /dev/null
+++ b/utils/qemu-boot/qemu-boot.c
@@ -0,0 +1,362 @@
+/* libguestfs
+ * Copyright (C) 2014-2016 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/* Ancient libguestfs had a script called test-bootbootboot which just
+ * booted up the appliance in a loop.  This was necessary back in the
+ * bad old days when qemu was not very reliable.  This is the
+ * spiritual successor of that script, designed to find bugs in
+ * aarch64 KVM.  You can control the number of boots that are done and
+ * the amount of parallelism.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <getopt.h>
+#include <limits.h>
+#include <errno.h>
+#include <pthread.h>
+
+#include "guestfs.h"
+#include "guestfs-internal-frontend.h"
+#include "estimate-max-threads.h"
+
+#define MIN(a,b) ((a)<(b)?(a):(b))
+
+/* Maximum number of threads we would ever run.  Note this should not
+ * be > 20, unless libvirt is modified to increase the maximum number
+ * of clients.  User can override this limit using -P.
+ */
+#define MAX_THREADS 12
+
+static size_t n;       /* Number of qemu processes to run in total. */
+static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
+
+static int ignore_errors = 0;
+static const char *log_template = NULL;
+static size_t log_file_size;
+static int trace = 0;
+static int verbose = 0;
+
+/* Events captured by the --log option. */
+static const uint64_t event_bitmask =
+  GUESTFS_EVENT_LIBRARY |
+  GUESTFS_EVENT_WARNING |
+  GUESTFS_EVENT_APPLIANCE |
+  GUESTFS_EVENT_TRACE;
+
+struct thread_data {
+  int thread_num;
+  int r;
+};
+
+static void *start_thread (void *thread_data_vp);
+static void message_callback (guestfs_h *g, void *opaque, uint64_t event, int event_handle, int flags, const char *buf, size_t buf_len, const uint64_t *array, size_t array_len);
+
+static void
+usage (int exitcode)
+{
+  fprintf (stderr,
+           "qemu-boot: A program for repeatedly running the libguestfs appliance.\n"
+           "qemu-boot [-i] [--log output.%%] [-P <nr-threads>] -n <nr-appliances>\n"
+           "  -i     Ignore errors\n"
+           "  --log <file.%%>\n"
+           "         Write per-appliance logs to file (%% in name replaced by boot number)\n"
+           "  -P <n> Set number of parallel threads\n"
+           "           (default is based on the amount of free memory)\n"
+           "  -n <n> Set number of appliances to run before exiting\n"
+           "  -v     Verbose appliance\n"
+           "  -x     Enable libguestfs tracing\n");
+  exit (exitcode);
+}
+
+int
+main (int argc, char *argv[])
+{
+  enum { HELP_OPTION = CHAR_MAX + 1 };
+  static const char options[] = "in:P:vx";
+  static const struct option long_options[] = {
+    { "help", 0, 0, HELP_OPTION },
+    { "ignore", 0, 0, 'i' },
+    { "log", 1, 0, 0 },
+    { "number", 1, 0, 'n' },
+    { "processes", 1, 0, 'P' },
+    { "trace", 0, 0, 'x' },
+    { "verbose", 0, 0, 'v' },
+    { 0, 0, 0, 0 }
+  };
+  size_t P = 0, i, errors;
+  int c, option_index;
+  int err;
+  void *status;
+
+  for (;;) {
+    c = getopt_long (argc, argv, options, long_options, &option_index);
+    if (c == -1) break;
+
+    switch (c) {
+    case 0:
+      /* Options which are long only. */
+      if (STREQ (long_options[option_index].name, "log")) {
+        log_template = optarg;
+        log_file_size = strlen (log_template);
+        for (i = 0; i < strlen (log_template); ++i) {
+          if (log_template[i] == '%')
+            log_file_size += 64;
+        }
+      }
+      else {
+        fprintf (stderr, "%s: unknown long option: %s (%d)\n",
+                 guestfs_int_program_name, long_options[option_index].name, option_index);
+        exit (EXIT_FAILURE);
+      }
+      break;
+
+    case 'i':
+      ignore_errors = 1;
+      break;
+
+    case 'n':
+      if (sscanf (optarg, "%zu", &n) != 1 || n == 0) {
+        fprintf (stderr, "%s: -n option not numeric and greater than 0\n",
+                 guestfs_int_program_name);
+        exit (EXIT_FAILURE);
+      }
+      break;
+
+    case 'P':
+      if (sscanf (optarg, "%zu", &P) != 1) {
+        fprintf (stderr, "%s: -P option not numeric\n", guestfs_int_program_name);
+        exit (EXIT_FAILURE);
+      }
+      break;
+
+    case 'v':
+      verbose = 1;
+      break;
+
+    case 'x':
+      trace = 1;
+      break;
+
+    case HELP_OPTION:
+      usage (EXIT_SUCCESS);
+
+    default:
+      usage (EXIT_FAILURE);
+    }
+  }
+
+  if (n == 0) {
+    fprintf (stderr,
+             "%s: must specify number of processes to run (-n option)\n",
+             guestfs_int_program_name);
+    exit (EXIT_FAILURE);
+  }
+
+  if (optind != argc) {
+    fprintf (stderr, "%s: extra arguments found on the command line\n",
+             guestfs_int_program_name);
+    exit (EXIT_FAILURE);
+  }
+
+  /* Calculate the number of threads to use. */
+  if (P > 0)
+    P = MIN (n, P);
+  else
+    P = MIN (n, MIN (MAX_THREADS, estimate_max_threads ()));
+
+  /* Start the worker threads. */
+  struct thread_data thread_data[P];
+  pthread_t threads[P];
+
+  for (i = 0; i < P; ++i) {
+    thread_data[i].thread_num = i;
+    err = pthread_create (&threads[i], NULL, start_thread, &thread_data[i]);
+    if (err != 0) {
+      fprintf (stderr, "%s: pthread_create[%zu]: %s\n",
+               guestfs_int_program_name, i, strerror (err));
+      exit (EXIT_FAILURE);
+    }
+  }
+
+  /* Wait for the threads to exit. */
+  errors = 0;
+  for (i = 0; i < P; ++i) {
+    err = pthread_join (threads[i], &status);
+    if (err != 0) {
+      fprintf (stderr, "%s: pthread_join[%zu]: %s\n",
+               guestfs_int_program_name, i, strerror (err));
+      errors++;
+    }
+    if (*(int *)status == -1)
+      errors++;
+  }
+
+  exit (errors == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
+}
+
+/* Worker thread. */
+static void *
+start_thread (void *thread_data_vp)
+{
+  struct thread_data *thread_data = thread_data_vp;
+  int quit = 0;
+  int err;
+  size_t i;
+  guestfs_h *g;
+  unsigned errors = 0;
+  char id[64];
+
+  for (;;) {
+    CLEANUP_FREE char *log_file = NULL;
+    CLEANUP_FCLOSE FILE *log_fp = NULL;
+
+    /* Take the next process. */
+    err = pthread_mutex_lock (&mutex);
+    if (err != 0) {
+      fprintf (stderr, "%s: pthread_mutex_lock: %s",
+               guestfs_int_program_name, strerror (err));
+      goto error;
+    }
+
+    i = n;
+    if (i > 0) {
+      printf ("%zu to go ...          \r", n);
+      fflush (stdout);
+
+      n--;
+    }
+    else
+      quit = 1;
+
+    err = pthread_mutex_unlock (&mutex);
+    if (err != 0) {
+      fprintf (stderr, "%s: pthread_mutex_unlock: %s",
+               guestfs_int_program_name, strerror (err));
+      goto error;
+    }
+
+    if (quit)                   /* Work finished. */
+      break;
+
+    g = guestfs_create ();
+    if (g == NULL) {
+      perror ("guestfs_create");
+      errors++;
+      if (!ignore_errors)
+        goto error;
+    }
+
+    /* Only if using --log, set up a callback.  See examples/debug-logging.c */
+    if (log_template != NULL) {
+      size_t j, k;
+
+      log_file = malloc (log_file_size + 1);
+      if (log_file == NULL) abort ();
+      for (j = 0, k = 0; j < strlen (log_template); ++j) {
+        if (log_template[j] == '%') {
+          snprintf (&log_file[k], log_file_size - k, "%zu", i);
+          k += strlen (&log_file[k]);
+        }
+        else
+          log_file[k++] = log_template[j];
+      }
+      log_file[k] = '\0';
+      log_fp = fopen (log_file, "w");
+      if (log_fp == NULL) {
+        perror (log_file);
+        abort ();
+      }
+      guestfs_set_event_callback (g, message_callback,
+                                  event_bitmask, 0, log_fp);
+    }
+
+    snprintf (id, sizeof id, "%zu", i);
+    guestfs_set_identifier (g, id);
+
+    guestfs_set_trace (g, trace);
+    guestfs_set_verbose (g, verbose);
+
+    if (guestfs_add_drive_ro (g, "/dev/null") == -1) {
+      errors++;
+      if (!ignore_errors)
+        goto error;
+    }
+
+    if (guestfs_launch (g) == -1) {
+      errors++;
+      if (!ignore_errors)
+        goto error;
+    }
+
+    if (guestfs_shutdown (g) == -1) {
+      errors++;
+      if (!ignore_errors)
+        goto error;
+    }
+
+    guestfs_close (g);
+  }
+
+  if (errors > 0) {
+    fprintf (stderr, "%s: thread %d: %u errors were ignored\n",
+             guestfs_int_program_name, thread_data->thread_num, errors);
+    goto error;
+  }
+
+  thread_data->r = 0;
+  return &thread_data->r;
+
+ error:
+  thread_data->r = -1;
+  return &thread_data->r;
+}
+
+/* If using --log, this is called to write messages to the log file. */
+static void
+message_callback (guestfs_h *g, void *opaque,
+                  uint64_t event, int event_handle,
+                  int flags,
+                  const char *buf, size_t buf_len,
+                  const uint64_t *array, size_t array_len)
+{
+  FILE *fp = opaque;
+
+  if (buf_len > 0) {
+    CLEANUP_FREE char *msg = strndup (buf, buf_len);
+
+    switch (event) {
+    case GUESTFS_EVENT_APPLIANCE:
+      fprintf (fp, "%s", msg);
+      break;
+    case GUESTFS_EVENT_LIBRARY:
+      fprintf (fp, "libguestfs: %s\n", msg);
+      break;
+    case GUESTFS_EVENT_WARNING:
+      fprintf (fp, "libguestfs: warning: %s\n", msg);
+      break;
+    case GUESTFS_EVENT_TRACE:
+      fprintf (fp, "libguestfs: trace: %s\n", msg);
+      break;
+    }
+    fflush (fp);
+  }
+}
diff --git a/utils/qemu-speed-test/Makefile.am b/utils/qemu-speed-test/Makefile.am
new file mode 100644
index 0000000..bf1c915
--- /dev/null
+++ b/utils/qemu-speed-test/Makefile.am
@@ -0,0 +1,36 @@
+# libguestfs
+# Copyright (C) 2011-2016 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+include $(top_srcdir)/subdir-rules.mk
+
+noinst_PROGRAMS = qemu-speed-test
+
+qemu_speed_test_SOURCES = \
+	qemu-speed-test.c
+qemu_speed_test_CPPFLAGS = \
+	-I$(top_srcdir)/gnulib/lib -I$(top_builddir)/gnulib/lib \
+	-I$(top_srcdir)/src -I$(top_builddir)/src \
+	-I$(top_srcdir)/df
+qemu_speed_test_CFLAGS = \
+	$(WARN_CFLAGS) $(WERROR_CFLAGS)
+qemu_speed_test_LDADD = \
+	$(top_builddir)/src/libutils.la \
+	$(top_builddir)/src/libguestfs.la \
+	$(LIBXML2_LIBS) \
+	$(LIBVIRT_LIBS) \
+	$(LTLIBINTL) \
+	$(top_builddir)/gnulib/lib/libgnu.la
diff --git a/utils/qemu-speed-test/qemu-speed-test.c b/utils/qemu-speed-test/qemu-speed-test.c
new file mode 100644
index 0000000..d5e34c3
--- /dev/null
+++ b/utils/qemu-speed-test/qemu-speed-test.c
@@ -0,0 +1,480 @@
+/* libguestfs
+ * Copyright (C) 2014 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/* Test the speed of various qemu features.  Currently tested are:
+ *   - virtio-serial upload
+ *   - virtio-serial download
+ *   - block device read
+ *   - block device write
+ * More to come in future.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <inttypes.h>
+#include <errno.h>
+#include <getopt.h>
+#include <unistd.h>
+#include <signal.h>
+#include <assert.h>
+#include <sys/time.h>
+
+#include "guestfs.h"
+#include "guestfs-internal-frontend.h"
+
+static void test_virtio_serial (void);
+static void test_block_device (void);
+
+/* Which tests are enabled? -- All by default. */
+static int virtio_serial_upload = 1;
+static int virtio_serial_download = 1;
+static int block_device_write = 1;
+static int block_device_read = 1;
+
+static int max_time_override = 0;
+
+static void
+reset_default_tests (int *flag)
+{
+  if (*flag) {
+    virtio_serial_upload = 0;
+    virtio_serial_download = 0;
+    block_device_write = 0;
+    block_device_read = 0;
+    *flag = 0;
+  }
+}
+
+static void
+usage (int exitcode)
+{
+  fprintf (stderr,
+           "qemu-speed-test: Test the speed of qemu features.\n"
+           "\n"
+           "To run all tests (recommended), do:\n"
+           "  qemu-speed-test\n"
+           "\n"
+           "To run only specific tests, do:\n"
+           "  qemu-speed-test --option [--option ...]\n"
+           "where the test options are:\n"
+           "  --virtio-serial-upload\n"
+           "  --virtio-serial-download\n"
+           "  --block-device-write\n"
+           "  --block-device-read\n"
+           "\n"
+           "Other options:\n"
+           "  --help                       Display help output and exit\n"
+           "  -t <SECS> | --time=<SECS>    Set max length of test in seconds\n"
+           );
+  exit (exitcode);
+}
+
+int
+main (int argc, char *argv[])
+{
+  enum { HELP_OPTION = CHAR_MAX + 1 };
+  static const char options[] = "t:";
+  static const struct option long_options[] = {
+    { "help", 0, 0, HELP_OPTION },
+    { "time", 1, 0, 't' },
+
+    /* Tests. */
+    { "virtio-serial-upload", 0, 0, 0 },
+    { "virtio-serial-download", 0, 0, 0 },
+    { "block-device-write", 0, 0, 0 },
+    { "block-device-read", 0, 0, 0 },
+
+    { 0, 0, 0, 0 }
+  };
+  int c, option_index;
+  int reset_flag = 1;
+
+  for (;;) {
+    c = getopt_long (argc, argv, options, long_options, &option_index);
+    if (c == -1) break;
+
+    switch (c) {
+    case 0:
+      /* Options which are long only. */
+      if (STREQ (long_options[option_index].name, "virtio-serial-upload")) {
+        reset_default_tests (&reset_flag);
+        virtio_serial_upload = 1;
+      }
+      else if (STREQ (long_options[option_index].name, "virtio-serial-download")) {
+        reset_default_tests (&reset_flag);
+        virtio_serial_download = 1;
+      }
+      else if (STREQ (long_options[option_index].name, "block-device-write")) {
+        reset_default_tests (&reset_flag);
+        block_device_write = 1;
+      }
+      else if (STREQ (long_options[option_index].name, "block-device-read")) {
+        reset_default_tests (&reset_flag);
+        block_device_read = 1;
+      }
+      else {
+        fprintf (stderr, "%s: unknown long option: %s (%d)\n",
+                 guestfs_int_program_name, long_options[option_index].name, option_index);
+        exit (EXIT_FAILURE);
+      }
+      break;
+
+    case 't':
+      if (sscanf (optarg, "%d", &max_time_override) != 1 ||
+          max_time_override < 0) {
+        fprintf (stderr, "%s: -t: argument is not a positive integer\n",
+                 guestfs_int_program_name);
+        exit (EXIT_FAILURE);
+      }
+      break;
+
+    case HELP_OPTION:
+      usage (EXIT_SUCCESS);
+
+    default:
+      usage (EXIT_FAILURE);
+    }
+  }
+
+  if (optind != argc) {
+    fprintf (stderr, "%s: extra arguments found on the command line\n",
+             guestfs_int_program_name);
+    exit (EXIT_FAILURE);
+  }
+
+  test_virtio_serial ();
+  test_block_device ();
+
+  exit (EXIT_SUCCESS);
+}
+
+static void
+print_rate (const char *msg, int64_t rate)
+{
+  printf ("%-40s %" PRIi64 " bytes/sec (%" PRIi64 " Mbytes/sec)\n",
+          msg, rate, rate / 1024 / 1024);
+  fflush (stdout);
+}
+
+/* The maximum time we will spend running the test (seconds). */
+#define TEST_SERIAL_MAX_TIME 30
+
+/* The maximum amount of data to copy.  You can safely make this very
+ * large because it's only making sparse files.
+ */
+#define TEST_SERIAL_MAX_SIZE						\
+  (INT64_C(1024) * INT64_C(1024) * INT64_C(1024) * INT64_C(1024))
+
+static guestfs_h *g;
+static struct timeval start;
+static const char *operation;
+static int64_t rate;
+
+static void
+stop_transfer (int sig)
+{
+  guestfs_user_cancel (g);
+}
+
+/* Compute Y - X and return the result in milliseconds.
+ * Approximately the same as this code:
+ * http://www.mpp.mpg.de/~huber/util/timevaldiff.c
+ */
+static int64_t
+timeval_diff (const struct timeval *x, const struct timeval *y)
+{
+  int64_t msec;
+
+  msec = (y->tv_sec - x->tv_sec) * 1000;
+  msec += (y->tv_usec - x->tv_usec) / 1000;
+  return msec;
+}
+
+static void
+progress_cb (guestfs_h *g, void *vp, uint64_t event,
+             int eh, int flags,
+             const char *buf, size_t buflen,
+             const uint64_t *array, size_t arraylen)
+{
+  uint64_t transferred;
+  struct timeval now;
+  int64_t millis;
+
+  assert (event == GUESTFS_EVENT_PROGRESS);
+  assert (arraylen >= 4);
+
+  gettimeofday (&now, NULL);
+
+  /* Bytes transferred. */
+  transferred = array[2];
+
+  /* Calculate the speed of the upload or download. */
+  millis = timeval_diff (&start, &now);
+  assert (millis >= 0);
+
+  if (millis != 0) {
+    rate = 1000 * transferred / millis;
+    printf ("%s: %" PRIi64 " bytes/sec          \r",
+            operation, rate);
+    fflush (stdout);
+  }
+}
+
+static void
+test_virtio_serial (void)
+{
+  int fd, r, eh;
+  char tmpfile[] = "/tmp/speedtestXXXXXX";
+  struct sigaction sa, old_sa;
+
+  if (!virtio_serial_upload && !virtio_serial_download)
+    return;
+
+  /* Create a sparse file.  We could upload from /dev/zero, but we
+   * won't get progress messages because libguestfs tests if the
+   * source file is a regular file.
+   */
+  fd = mkstemp (tmpfile);
+  if (fd == -1) {
+    perror ("mkstemp");
+    exit (EXIT_FAILURE);
+  }
+  if (ftruncate (fd, TEST_SERIAL_MAX_SIZE) == -1) {
+    perror ("ftruncate");
+    exit (EXIT_FAILURE);
+  }
+  if (close (fd) == -1) {
+    perror ("close");
+    exit (EXIT_FAILURE);
+  }
+
+  g = guestfs_create ();
+  if (!g) {
+    perror ("guestfs_create");
+    exit (EXIT_FAILURE);
+  }
+
+  if (guestfs_add_drive_scratch (g, INT64_C (100*1024*1024), -1) == -1)
+    exit (EXIT_FAILURE);
+
+  if (guestfs_launch (g) == -1)
+    exit (EXIT_FAILURE);
+
+  /* Make and mount a filesystem which will be used by the download test. */
+  if (guestfs_mkfs (g, "ext4", "/dev/sda") == -1)
+    exit (EXIT_FAILURE);
+  if (guestfs_mount (g, "/dev/sda", "/") == -1)
+    exit (EXIT_FAILURE);
+
+  /* Time out the upload after TEST_SERIAL_MAX_TIME seconds have passed. */
+  memset (&sa, 0, sizeof sa);
+  sa.sa_handler = stop_transfer;
+  sa.sa_flags = SA_RESTART;
+  sigaction (SIGALRM, &sa, &old_sa);
+
+  /* Get progress messages, which will tell us how much data has been
+   * transferred.
+   */
+  eh = guestfs_set_event_callback (g, progress_cb, GUESTFS_EVENT_PROGRESS,
+                                   0, NULL);
+  if (eh == -1)
+    exit (EXIT_FAILURE);
+
+  if (virtio_serial_upload) {
+    gettimeofday (&start, NULL);
+    rate = -1;
+    operation = "upload";
+    alarm (max_time_override > 0 ? max_time_override : TEST_SERIAL_MAX_TIME);
+
+    /* For the upload test, upload the sparse file to /dev/null in the
+     * appliance.  Hopefully this is mostly testing just virtio-serial.
+     */
+    guestfs_push_error_handler (g, NULL, NULL);
+    r = guestfs_upload (g, tmpfile, "/dev/null");
+    alarm (0);
+    unlink (tmpfile);
+    guestfs_pop_error_handler (g);
+
+    /* It's possible that the upload will finish before the alarm fires,
+     * or that the upload will be stopped by the alarm.
+     */
+    if (r == -1 && guestfs_last_errno (g) != EINTR) {
+      fprintf (stderr,
+               "%s: expecting upload command to return EINTR\n%s\n",
+               guestfs_int_program_name, guestfs_last_error (g));
+      exit (EXIT_FAILURE);
+    }
+
+    if (rate == -1) {
+    rate_error:
+      fprintf (stderr, "%s: internal error: progress callback was not called! (r=%d, errno=%d)\n",
+               guestfs_int_program_name,
+               r, guestfs_last_errno (g));
+      exit (EXIT_FAILURE);
+    }
+
+    print_rate ("virtio-serial upload rate:", rate);
+  }
+
+  if (virtio_serial_download) {
+    /* For the download test, download a sparse file within the
+     * appliance to /dev/null on the host.
+     */
+    if (guestfs_touch (g, "/sparse") == -1)
+      exit (EXIT_FAILURE);
+    if (guestfs_truncate_size (g, "/sparse", TEST_SERIAL_MAX_SIZE) == -1)
+      exit (EXIT_FAILURE);
+
+    gettimeofday (&start, NULL);
+    rate = -1;
+    operation = "download";
+    alarm (max_time_override > 0 ? max_time_override : TEST_SERIAL_MAX_TIME);
+    guestfs_push_error_handler (g, NULL, NULL);
+    r = guestfs_download (g, "/sparse", "/dev/null");
+    alarm (0);
+    guestfs_pop_error_handler (g);
+
+    if (r == -1 && guestfs_last_errno (g) != EINTR) {
+      fprintf (stderr,
+               "%s: expecting download command to return EINTR\n%s\n",
+               guestfs_int_program_name, guestfs_last_error (g));
+      exit (EXIT_FAILURE);
+    }
+
+    if (rate == -1)
+      goto rate_error;
+
+    print_rate ("virtio-serial download rate:", rate);
+  }
+
+  if (guestfs_shutdown (g) == -1)
+    exit (EXIT_FAILURE);
+
+  guestfs_close (g);
+
+  /* Restore SIGALRM signal handler. */
+  sigaction (SIGALRM, &old_sa, NULL);
+}
+
+/* The time we will spend running the test (seconds). */
+#define TEST_BLOCK_DEVICE_TIME 30
+
+static void
+test_block_device (void)
+{
+  int fd;
+  char tmpfile[] = "/tmp/speedtestXXXXXX";
+  CLEANUP_FREE char **devices = NULL;
+  char *r;
+  const char *argv[4];
+  const int t =
+    max_time_override > 0 ? max_time_override : TEST_BLOCK_DEVICE_TIME;
+  char tbuf[64];
+  int64_t bytes_written, bytes_read;
+
+  if (!block_device_write && !block_device_read)
+    return;
+
+  snprintf (tbuf, sizeof tbuf, "%d", t);
+
+  g = guestfs_create ();
+  if (!g) {
+    perror ("guestfs_create");
+    exit (EXIT_FAILURE);
+  }
+
+  /* Create a fully allocated backing file.  Note we are not testing
+   * the speed of allocation on the host.
+   */
+  fd = mkstemp (tmpfile);
+  if (fd == -1) {
+    perror ("mkstemp");
+    exit (EXIT_FAILURE);
+  }
+  close (fd);
+
+  if (guestfs_disk_create (g, tmpfile, "raw",
+                           INT64_C (1024*1024*1024),
+                           GUESTFS_DISK_CREATE_PREALLOCATION, "full",
+                           -1) == -1)
+    exit (EXIT_FAILURE);
+
+  if (guestfs_add_drive (g, tmpfile) == -1)
+    exit (EXIT_FAILURE);
+
+  if (guestfs_launch (g) == -1)
+    exit (EXIT_FAILURE);
+
+  devices = guestfs_list_devices (g);
+  if (devices == NULL)
+    exit (EXIT_FAILURE);
+  if (devices[0] == NULL) {
+    fprintf (stderr, "%s: expected guestfs_list_devices to return at least 1 device\n",
+             guestfs_int_program_name);
+    exit (EXIT_FAILURE);
+  }
+
+  if (block_device_write) {
+    /* Test write speed. */
+    argv[0] = devices[0];
+    argv[1] = "w";
+    argv[2] = tbuf;
+    argv[3] = NULL;
+    r = guestfs_debug (g, "device_speed", (char **) argv);
+    if (r == NULL)
+      exit (EXIT_FAILURE);
+
+    if (sscanf (r, "%" SCNi64, &bytes_written) != 1) {
+      fprintf (stderr, "%s: could not parse device_speed output\n",
+               guestfs_int_program_name);
+      exit (EXIT_FAILURE);
+    }
+
+    print_rate ("block device writes:", bytes_written / t);
+  }
+
+  if (block_device_read) {
+    /* Test read speed. */
+    argv[0] = devices[0];
+    argv[1] = "r";
+    argv[2] = tbuf;
+    argv[3] = NULL;
+    r = guestfs_debug (g, "device_speed", (char **) argv);
+    if (r == NULL)
+      exit (EXIT_FAILURE);
+
+    if (sscanf (r, "%" SCNi64, &bytes_read) != 1) {
+      fprintf (stderr, "%s: could not parse device_speed output\n",
+               guestfs_int_program_name);
+      exit (EXIT_FAILURE);
+    }
+
+    print_rate ("block device reads:", bytes_read / t);
+  }
+
+  if (guestfs_shutdown (g) == -1)
+    exit (EXIT_FAILURE);
+
+  guestfs_close (g);
+
+  /* Remove temporary file. */
+  unlink (tmpfile);
+}
-- 
2.7.4