Add a contrib directory which we can use to store random tests and
other contributions that we don't necessarily want to support long
term or incorporate into nbdkit.
Into this directory place a test program which tests
nbdkit-memory-plugin with allocator=sparse.
---
configure.ac | 1 +
Makefile.am | 1 +
contrib/Makefile.am | 44 +++++
contrib/sparseloadtest.c | 399 +++++++++++++++++++++++++++++++++++++++
.gitignore | 1 +
5 files changed, 446 insertions(+)
diff --git a/configure.ac b/configure.ac
index fc4928b5d..d8a93ac14 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1470,6 +1470,7 @@ AC_CONFIG_FILES([Makefile
common/replacements/Makefile
common/replacements/win32/Makefile
common/utils/Makefile
+ contrib/Makefile
docs/Makefile
include/Makefile
include/nbdkit-version.h
diff --git a/Makefile.am b/Makefile.am
index b1fcc815a..3fe149909 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -84,6 +84,7 @@ SUBDIRS = \
common/replacements \
common/utils \
server \
+ contrib
$(NULL)
if HAVE_PLUGINS
diff --git a/contrib/Makefile.am b/contrib/Makefile.am
new file mode 100644
index 000000000..b9866e47d
--- /dev/null
+++ b/contrib/Makefile.am
@@ -0,0 +1,44 @@
+# nbdkit
+# Copyright Red Hat
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# * Neither the name of Red Hat nor the names of its contributors may be
+# used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+include $(top_srcdir)/common-rules.mk
+
+if HAVE_LIBNBD
+
+noinst_PROGRAMS = sparseloadtest
+
+sparseloadtest_SOURCES = sparseloadtest.c
+sparseloadtest_CPPFLAGS = -I$(top_srcdir)/common/include
+sparseloadtest_CFLAGS = $(PTHREAD_CFLAGS) $(WARNINGS_CFLAGS) $(LIBNBD_CFLAGS)
+sparseloadtest_LDADD = $(LIBNBD_LIBS)
+sparseloadtest_LDFLAGS = $(PTHREAD_LIBS)
+
+endif HAVE_LIBNBD
diff --git a/contrib/sparseloadtest.c b/contrib/sparseloadtest.c
new file mode 100644
index 000000000..4aa3f5104
--- /dev/null
+++ b/contrib/sparseloadtest.c
@@ -0,0 +1,399 @@
+/* nbdkit
+ * Copyright Red Hat
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of Red Hat nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/* Load nbdkit-memory-plugin + allocator=sparse
+ *
+ * This is better than fio for this plugin since it exercises
+ * allocation and deallocation of pages and locking.
+ *
+ * To test a mainly read workload (90% reads, 10% writes, 10% trims):
+ * ./contrib/sparseloadtest 4 90
+ *
+ * To test a write-heavy workload (20% reads, 40% writes, 40% trims):
+ * ./contrib/sparseloadtest 4 20
+ *
+ * nbdkit is run from the current $PATH environment variable, so to
+ * run the locally built nbdkit you should do:
+ *
+ * PATH=.:$PATH ./contrib/sparseloadtest [...]
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+#include <signal.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <pthread.h>
+
+#include <libnbd.h>
+
+#include "random.h"
+
+#define PAGE_SIZE 32768 /* See common/allocators/sparse.c */
+#define L2_SIZE 4096
+#define DISK_SIZE (4*L2_SIZE*PAGE_SIZE)
+#define MAX_THREADS 16
+#define DURATION 60 /* seconds */
+#define MAX_IN_FLIGHT 64
+#define MAX_REQUEST (128*1024) /* Should be larger than PAGE_SIZE. */
+
+static pid_t pid;
+static char sockfile[] = "/tmp/sockXXXXXX";
+static char pidfile[] = "/tmp/pidXXXXXX";
+static unsigned nr_threads;
+static double pc_read; /* % read operations. */
+
+struct stats {
+ size_t ops;
+ size_t bytes;
+};
+
+struct thread_data {
+ pthread_t thread;
+ int status; /* returned status from the thread */
+ struct nbd_handle *nbd; /* per-thread handle */
+ struct stats read_stats, write_stats, trim_stats;
+ struct random_state state;
+};
+static struct thread_data thread[MAX_THREADS];
+
+static time_t start_t;
+
+struct command_data {
+ struct stats *stats;
+ size_t count;
+};
+
+/* We don't care about the data that is read, so this is just a sink
+ * buffer shared across all threads.
+ */
+static char sink[MAX_REQUEST];
+
+static char wrbuf[MAX_REQUEST];
+
+static void start_nbdkit (void);
+static void create_random_name (char *template);
+static void cleanup (void);
+static void *start_thread (void *);
+
+int
+main (int argc, char *argv[])
+{
+ unsigned i;
+ int err;
+ struct stats read_total = { 0 };
+ struct stats write_total = { 0 };
+ struct stats trim_total = { 0 };
+
+ if (argc != 3) {
+ fprintf (stderr, "%s nr_threads percent_reads\n", argv[0]);
+ exit (EXIT_FAILURE);
+ }
+ if (sscanf (argv[1], "%u", &nr_threads) != 1 ||
+ nr_threads == 0 || nr_threads > MAX_THREADS ||
+ sscanf (argv[2], "%lg", &pc_read) != 1 ||
+ pc_read <= 0 || pc_read > 100) {
+ fprintf (stderr, "%s: incorrect parameters, read the source!\n", argv[0]);
+ exit (EXIT_FAILURE);
+ }
+
+ start_nbdkit ();
+ atexit (cleanup);
+
+ /* Connect to nbdkit. */
+ for (i = 0; i < nr_threads; ++i) {
+ struct nbd_handle *nbd = nbd_create ();
+ if (nbd == NULL) {
+ got_nbd_error:
+ fprintf (stderr, "%s: thread %u: %s\n", argv[0], i, nbd_get_error ());
+ exit (EXIT_FAILURE);
+ }
+ if (nbd_connect_unix (nbd, sockfile) == -1)
+ goto got_nbd_error;
+ thread[i].nbd = nbd;
+ }
+
+ time (&start_t);
+
+ /* Start threads. */
+ for (i = 0; i < nr_threads; ++i) {
+ xsrandom (i+1, &thread[i].state);
+
+ if (i == 0) {
+ size_t j;
+ for (j = 0; j < sizeof wrbuf; ++j)
+ wrbuf[j] = xrandom (&thread[i].state);
+ }
+
+ err = pthread_create (&thread[i].thread, NULL, start_thread, &thread[i]);
+ if (err != 0) {
+ errno = err;
+ perror ("pthread_create");
+ exit (EXIT_FAILURE);
+ }
+ }
+
+ /* Wait for the threads to exit. */
+ for (i = 0; i < nr_threads; ++i) {
+ err = pthread_join (thread[i].thread, NULL);
+ if (err != 0) {
+ errno = err;
+ perror ("pthread_join");
+ exit (EXIT_FAILURE);
+ }
+ if (thread[i].status != 0) {
+ fprintf (stderr, "%s: thread %u failed, see earlier errors\n",
+ argv[0], i);
+ exit (EXIT_FAILURE);
+ }
+
+ read_total.ops += thread[i].read_stats.ops;
+ read_total.bytes += thread[i].read_stats.bytes;
+ write_total.ops += thread[i].write_stats.ops;
+ write_total.bytes += thread[i].write_stats.bytes;
+ trim_total.ops += thread[i].trim_stats.ops;
+ trim_total.bytes += thread[i].trim_stats.bytes;
+
+ /* Drain the command queue just to avoid errors. These requests
+ * don't count in the final totals.
+ */
+ while (nbd_aio_in_flight (thread[i].nbd) > 0) {
+ if (nbd_poll (thread[i].nbd, -1) == -1) {
+ fprintf (stderr, "%s\n", nbd_get_error ());
+ exit (EXIT_FAILURE);
+ }
+ }
+
+ nbd_close (thread[i].nbd);
+ }
+
+ /* Print the throughput. */
+ printf ("READ: %.1f ops/s %.1f bytes/s\n",
+ (double) read_total.ops / DURATION,
+ (double) read_total.bytes / DURATION);
+ printf ("WRITE: %.1f ops/s %.1f bytes/s\n",
+ (double) write_total.ops / DURATION,
+ (double) write_total.bytes / DURATION);
+ printf ("TRIM: %.1f ops/s %.1f bytes/s\n",
+ (double) trim_total.ops / DURATION,
+ (double) trim_total.bytes / DURATION);
+ printf ("TOTAL: %.1f ops/s %.1f bytes/s\n",
+ (double) (read_total.ops + write_total.ops + trim_total.ops)
+ / DURATION,
+ (double) (read_total.bytes + write_total.bytes + trim_total.bytes)
+ / DURATION);
+ printf ("--\n");
+ printf ("%%read operations requested: %.1f%%, achieved: %.1f%%\n",
+ pc_read,
+ 100.0 * read_total.ops /
+ (read_total.ops + write_total.ops + trim_total.ops));
+ printf ("%%write operations requested: %.1f%%, achieved: %.1f%%\n",
+ (100 - pc_read) / 2,
+ 100.0 * write_total.ops /
+ (read_total.ops + write_total.ops + trim_total.ops));
+ printf ("%%trim operations requested: %.1f%%, achieved: %.1f%%\n",
+ (100 - pc_read) / 2,
+ 100.0 * trim_total.ops /
+ (read_total.ops + write_total.ops + trim_total.ops));
+
+ exit (EXIT_SUCCESS);
+}
+
+static void
+cleanup (void)
+{
+ if (pid > 0) {
+ kill (pid, SIGTERM);
+ unlink (sockfile);
+ pid = 0;
+ }
+}
+
+/* Start nbdkit.
+ *
+ * We cannot use systemd socket activation because we want to use
+ * multi-conn.
+ */
+static void
+start_nbdkit (void)
+{
+ char size[64];
+ int i;
+
+ snprintf (size, sizeof size, "%d", DISK_SIZE);
+
+ create_random_name (sockfile);
+ create_random_name (pidfile);
+
+ /* Start nbdkit. */
+ pid = fork ();
+ if (pid == -1) {
+ perror ("fork");
+ exit (EXIT_FAILURE);
+ }
+ if (pid == 0) { /* Child - nbdkit */
+ execlp ("nbdkit",
+ "nbdkit", "--exit-with-parent", "-f",
+ "-U", sockfile, "-P", pidfile,
+ "memory", size, "allocator=sparse",
+ NULL);
+ perror ("execlp");
+ _exit (EXIT_FAILURE);
+ }
+
+ /* Wait for the pidfile to appear, indicating that nbdkit is ready. */
+ for (i = 0; i < 60; ++i) {
+ if (access (pidfile, F_OK) == 0)
+ break;
+ sleep (1);
+ }
+ if (i == 60) {
+ fprintf (stderr, "nbdkit did not start up, look for errors above\n");
+ exit (EXIT_FAILURE);
+ }
+
+ unlink (pidfile);
+}
+
+/* This is racy but it doesn't matter for a test. */
+static void
+create_random_name (char *template)
+{
+ int fd;
+
+ fd = mkstemp (template);
+ if (fd == -1) {
+ perror (template);
+ exit (EXIT_FAILURE);
+ }
+ close (fd);
+ unlink (template);
+}
+
+static int
+cb (void *user_data, int *error)
+{
+ struct command_data *data = user_data;
+
+ if (*error != 0) {
+ fprintf (stderr, "unexpected error in completion callback\n");
+ exit (EXIT_FAILURE);
+ }
+
+ data->stats->ops++;
+ data->stats->bytes += data->count;
+
+ free (data);
+
+ return 1; /* retire the command */
+}
+
+static void *
+start_thread (void *thread_data_vp)
+{
+ struct thread_data *thread_data = thread_data_vp;
+ struct nbd_handle *nbd = thread_data->nbd;
+ time_t end_t;
+ size_t total_ops;
+ double pc_read_actual;
+ uint64_t offset;
+ size_t count;
+ struct command_data *data;
+ int64_t r;
+
+ while (time (&end_t), end_t - start_t < DURATION) {
+ /* Run the poll loop if there are too many requests in flight. */
+ while (nbd_aio_in_flight (nbd) >= MAX_IN_FLIGHT) {
+ if (nbd_poll (nbd, -1) == -1) {
+ fprintf (stderr, "%s\n", nbd_get_error ());
+ exit (EXIT_FAILURE);
+ }
+ }
+
+ /* Aim to send about pc_read% read operations, and an equal random
+ * distribution of writes and trims.
+ */
+ total_ops =
+ thread_data->read_stats.ops + thread_data->write_stats.ops +
+ thread_data->trim_stats.ops;
+ if (total_ops == 0)
+ pc_read_actual = 100;
+ else
+ pc_read_actual = 100 * (thread_data->read_stats.ops / (double) total_ops);
+
+ offset = xrandom (&thread_data->state) & (DISK_SIZE - MAX_REQUEST);
+ count = xrandom (&thread_data->state) & (MAX_REQUEST - 1);
+ if (count == 0) count = 1;
+
+ data = malloc (sizeof *data); /* freed in callback */
+ data->count = count;
+
+ if (pc_read_actual < pc_read) { /* read op */
+ data->stats = &thread_data->read_stats;
+ r = nbd_aio_pread (nbd, sink, count, offset,
+ (nbd_completion_callback) {
+ .callback = cb,
+ .user_data = data,
+ }, 0);
+ }
+ else {
+ if (xrandom (&thread_data->state) & 1) { /* write op */
+ data->stats = &thread_data->write_stats;
+ r = nbd_aio_pwrite (nbd, wrbuf, count, offset,
+ (nbd_completion_callback) {
+ .callback = cb,
+ .user_data = data,
+ }, 0);
+ }
+ else { /* trim op */
+ data->stats = &thread_data->trim_stats;
+ r = nbd_aio_trim (nbd, count, offset,
+ (nbd_completion_callback) {
+ .callback = cb,
+ .user_data = data,
+ }, 0);
+ }
+ }
+ if (r == -1) {
+ fprintf (stderr, "%s\n", nbd_get_error ());
+ exit (EXIT_FAILURE);
+ }
+ }
+
+ return NULL;
+}
diff --git a/.gitignore b/.gitignore
index 74b4e8361..e62c23d8b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,6 +66,7 @@ plugins/*/*.3
/config.status
/config.sub
/configure
+/contrib/sparseloadtest
/depcomp
/docs/filter-links.pod
/docs/lang-plugin-links.pod
--
2.39.2