diff --git a/.gitignore b/.gitignore index 5c614d88d..a29cf6d11 100644 --- a/.gitignore +++ b/.gitignore @@ -530,6 +530,7 @@ Makefile.in /tests/parallel/test-parallel /tests/protocol/test-error-messages /tests/qemu/qemu-boot +/tests/qemu/qemu-speed-test /tests/regressions/rhbz501893 /tests/regressions/rhbz790721 /tests/regressions/rhbz914931 diff --git a/daemon/debug.c b/daemon/debug.c index abc2bec3b..d29ca7860 100644 --- a/daemon/debug.c +++ b/daemon/debug.c @@ -1,5 +1,5 @@ /* libguestfs - the guestfsd daemon - * Copyright (C) 2009 Red Hat Inc. + * Copyright (C) 2009-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 @@ -64,6 +64,7 @@ struct cmd { static char *debug_help (const char *subcmd, size_t argc, char *const *const argv); static char *debug_binaries (const char *subcmd, size_t argc, char *const *const argv); static char *debug_core_pattern (const char *subcmd, size_t argc, char *const *const argv); +static char *debug_device_speed (const char *subcmd, size_t argc, char *const *const argv); static char *debug_env (const char *subcmd, size_t argc, char *const *const argv); static char *debug_error (const char *subcmd, size_t argc, char *const *const argv); static char *debug_fds (const char *subcmd, size_t argc, char *const *const argv); @@ -83,6 +84,7 @@ static struct cmd cmds[] = { { "help", debug_help }, { "binaries", debug_binaries }, { "core_pattern", debug_core_pattern }, + { "device_speed", debug_device_speed }, { "env", debug_env }, { "error", debug_error }, { "fds", debug_fds }, @@ -749,6 +751,130 @@ debug_qtrace (const char *subcmd, size_t argc, char *const *const argv) return ret; } +/* Used to test read and write speed. */ +static char * +debug_device_speed (const char *subcmd, size_t argc, char *const *const argv) +{ + const char *device; + int writing, err; + unsigned secs; + int64_t size, position, copied; + CLEANUP_FREE void *buf = NULL; + struct timeval now, end; + ssize_t r; + int fd; + char *ret; + + if (argc != 3) { + bad_args: + reply_with_error ("device_speed "); + return NULL; + } + + device = argv[0]; + if (STREQ (argv[1], "r") || STREQ (argv[1], "read")) + writing = 0; + else if (STREQ (argv[1], "w") || STREQ (argv[1], "write")) + writing = 1; + else + goto bad_args; + if (sscanf (argv[2], "%u", &secs) != 1) + goto bad_args; + + /* Find the size of the device. */ + size = do_blockdev_getsize64 (device); + if (size == -1) + return NULL; + + if (size < BUFSIZ) { + reply_with_error ("%s: device is too small", device); + return NULL; + } + + /* Because we're using O_DIRECT, the buffer must be aligned. */ + err = posix_memalign (&buf, 4096, BUFSIZ); + if (err != 0) { + reply_with_error_errno (err, "posix_memalign"); + return NULL; + } + + /* Any non-zero data will do. */ + memset (buf, 100, BUFSIZ); + + fd = open (device, (writing ? O_WRONLY : O_RDONLY) | O_CLOEXEC | O_DIRECT); + if (fd == -1) { + reply_with_perror ("open: %s", device); + return NULL; + } + + /* Now we read or write to the device, wrapping around to the + * beginning when we reach the end, and only stop when + * seconds has elapsed. + */ + gettimeofday (&end, NULL); + end.tv_sec += secs; + + position = copied = 0; + + for (;;) { + gettimeofday (&now, NULL); + if (now.tv_sec > end.tv_sec || + (now.tv_sec == end.tv_sec && now.tv_usec > end.tv_usec)) + break; + + /* Because of O_DIRECT, only write whole, aligned buffers. */ + again: + if (size - position < BUFSIZ) { + position = 0; + goto again; + } + + /* + if (verbose) { + fprintf (stderr, "p%s (fd, buf, %d, %" PRIi64 ")\n", + writing ? "write" : "read", BUFSIZ, position); + } + */ + + if (writing) { + r = pwrite (fd, buf, BUFSIZ, position); + if (r == -1) { + reply_with_perror ("write: %s", device); + goto error; + } + } + else { + r = pread (fd, buf, BUFSIZ, position); + if (r == -1) { + reply_with_perror ("read: %s", device); + goto error; + } + if (r == 0) { + reply_with_error ("unexpected end of file while reading"); + goto error; + } + } + position += BUFSIZ; + copied += r; + } + + if (close (fd) == -1) { + reply_with_perror ("close: %s", device); + return NULL; + } + + if (asprintf (&ret, "%" PRIi64, copied) == -1) { + reply_with_perror ("asprintf"); + return NULL; + } + + return ret; + + error: + close (fd); + return NULL; +} + /* Has one FileIn parameter. */ int do_debug_upload (const char *filename, int mode) diff --git a/tests/qemu/Makefile.am b/tests/qemu/Makefile.am index fa2fe92fe..b1b3555c5 100644 --- a/tests/qemu/Makefile.am +++ b/tests/qemu/Makefile.am @@ -30,12 +30,13 @@ TESTS_ENVIRONMENT = $(top_builddir)/run --test EXTRA_DIST = \ $(TESTS) \ - qemu-boot.c + qemu-boot.c \ + qemu-speed-test.c -# qemu-boot is built but not run by default as it is mainly -# a qemu & kernel diagnostic tool. +# qemu-boot & qemu-speed-test are built but not run by default as they +# are mainly qemu & kernel diagnostic tools. -check_PROGRAMS = qemu-boot +check_PROGRAMS = qemu-boot qemu-speed-test qemu_boot_SOURCES = \ ../../df/estimate-max-threads.c \ @@ -55,3 +56,19 @@ qemu_boot_LDADD = \ $(LIBXML2_LIBS) \ $(LIBVIRT_LIBS) \ $(top_builddir)/gnulib/lib/libgnu.la + +qemu_speed_test_SOURCES = \ + qemu-speed-test.c +qemu_speed_test_CPPFLAGS = \ + -DGUESTFS_PRIVATE=1 \ + -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) \ + $(top_builddir)/gnulib/lib/libgnu.la diff --git a/tests/qemu/qemu-speed-test.c b/tests/qemu/qemu-speed-test.c new file mode 100644 index 000000000..667b4737e --- /dev/null +++ b/tests/qemu/qemu-speed-test.c @@ -0,0 +1,356 @@ +/* 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "guestfs.h" +#include "guestfs-internal-frontend.h" + +static void test_virtio_serial (void); +static void test_block_device (void); + +int +main (int argc, char *argv[]) +{ + if (argc != 1) { + fprintf (stderr, "%s: this program takes no arguments\n", + 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; + + /* 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); + + gettimeofday (&start, NULL); + rate = -1; + operation = "upload"; + alarm (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", + 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", + program_name, + r, guestfs_last_errno (g)); + exit (EXIT_FAILURE); + } + + print_rate ("virtio-serial upload rate:", rate); + + /* 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 (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", + 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]; + int64_t bytes_written, bytes_read; + + 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", + program_name); + exit (EXIT_FAILURE); + } + + /* Test write speed. */ + argv[0] = devices[0]; + argv[1] = "w"; +#define XSTR(x) STR(x) +#define STR(x) #x + argv[2] = XSTR(TEST_BLOCK_DEVICE_TIME); + 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", + program_name); + exit (EXIT_FAILURE); + } + + print_rate ("block device writes:", bytes_written / TEST_BLOCK_DEVICE_TIME); + + /* Test read speed. */ + argv[0] = devices[0]; + argv[1] = "r"; +#define XSTR(x) STR(x) +#define STR(x) #x + argv[2] = XSTR(TEST_BLOCK_DEVICE_TIME); + 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", + program_name); + exit (EXIT_FAILURE); + } + + print_rate ("block device reads:", bytes_read / TEST_BLOCK_DEVICE_TIME); + + if (guestfs_shutdown (g) == -1) + exit (EXIT_FAILURE); + + guestfs_close (g); + + /* Remove temporary file. */ + unlink (tmpfile); +}