Merge branch 'evelu-uring' of https://github.com/ErwanAliasr1/fio
[fio.git] / t / io_uring.c
index 3bcb19b647ccc2c13c91098bf9a4231a12b05bfb..f22c504afc6ad3dbab725dcb1395ba725d7bb196 100644 (file)
@@ -5,6 +5,7 @@
 #include <stddef.h>
 #include <signal.h>
 #include <inttypes.h>
+#include <math.h>
 
 #include <sys/types.h>
 #include <sys/stat.h>
 
 #include "../arch/arch.h"
 #include "../lib/types.h"
+#include "../lib/roundup.h"
+#include "../minmax.h"
 #include "../os/linux/io_uring.h"
 
-#define min(a, b)              ((a < b) ? (a) : (b))
-
 struct io_sq_ring {
        unsigned *head;
        unsigned *tail;
@@ -44,10 +45,8 @@ struct io_cq_ring {
 };
 
 #define DEPTH                  128
-
 #define BATCH_SUBMIT           32
 #define BATCH_COMPLETE         32
-
 #define BS                     4096
 
 #define MAX_FDS                        16
@@ -59,33 +58,47 @@ struct file {
        unsigned pending_ios;
        int real_fd;
        int fixed_fd;
+       int fileno;
 };
 
+#define PLAT_BITS              6
+#define PLAT_VAL               (1 << PLAT_BITS)
+#define PLAT_GROUP_NR          29
+#define PLAT_NR                        (PLAT_GROUP_NR * PLAT_VAL)
+
 struct submitter {
        pthread_t thread;
        int ring_fd;
-       struct drand48_data rand;
+       int index;
        struct io_sq_ring sq_ring;
        struct io_uring_sqe *sqes;
-       struct iovec iovecs[DEPTH];
        struct io_cq_ring cq_ring;
        int inflight;
+       int tid;
        unsigned long reaps;
        unsigned long done;
        unsigned long calls;
-       unsigned long cachehit, cachemiss;
        volatile int finish;
 
        __s32 *fds;
 
+       unsigned long *clock_batch;
+       int clock_index;
+       unsigned long *plat;
+
        struct file files[MAX_FDS];
        unsigned nr_files;
        unsigned cur_file;
+       struct iovec iovecs[];
 };
 
-static struct submitter submitters[1];
+static struct submitter *submitter;
 static volatile int finish;
 
+static int depth = DEPTH;
+static int batch_submit = BATCH_SUBMIT;
+static int batch_complete = BATCH_COMPLETE;
+static int bs = BS;
 static int polled = 1;         /* use IO polling */
 static int fixedbufs = 1;      /* use fixed user buffers */
 static int register_files = 1; /* use fixed files */
@@ -93,14 +106,210 @@ static int buffered = 0;  /* use buffered IO, not O_DIRECT */
 static int sq_thread_poll = 0; /* use kernel submission/poller thread */
 static int sq_thread_cpu = -1; /* pin above thread to this CPU */
 static int do_nop = 0;         /* no-op SQ ring commands */
+static int nthreads = 1;
+static int stats = 0;          /* generate IO stats */
+static unsigned long tsc_rate;
+
+static int vectored = 1;
+
+static float plist[] = { 1.0, 5.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0,
+                       80.0, 90.0, 95.0, 99.9, 99.5, 99.9, 99.95, 99.99 };
+static int plist_len = 17;
+
+static unsigned long cycles_to_nsec(unsigned long cycles)
+{
+       uint64_t val;
+
+       if (!tsc_rate)
+               return cycles;
+
+       val = cycles * 1000000000ULL;
+       return val / tsc_rate;
+}
+
+static unsigned long plat_idx_to_val(unsigned int idx)
+{
+       unsigned int error_bits;
+       unsigned long k, base;
+
+       assert(idx < PLAT_NR);
+
+       /* MSB <= (PLAT_BITS-1), cannot be rounded off. Use
+        * all bits of the sample as index */
+       if (idx < (PLAT_VAL << 1))
+               return cycles_to_nsec(idx);
+
+       /* Find the group and compute the minimum value of that group */
+       error_bits = (idx >> PLAT_BITS) - 1;
+       base = ((unsigned long) 1) << (error_bits + PLAT_BITS);
+
+       /* Find its bucket number of the group */
+       k = idx % PLAT_VAL;
+
+       /* Return the mean of the range of the bucket */
+       return cycles_to_nsec(base + ((k + 0.5) * (1 << error_bits)));
+}
+
+unsigned int calc_clat_percentiles(unsigned long *io_u_plat, unsigned long nr,
+                                  unsigned long **output,
+                                  unsigned long *maxv, unsigned long *minv)
+{
+       unsigned long sum = 0;
+       unsigned int len = plist_len, i, j = 0;
+       unsigned long *ovals = NULL;
+       bool is_last;
+
+       *minv = -1ULL;
+       *maxv = 0;
+
+       ovals = malloc(len * sizeof(*ovals));
+       if (!ovals)
+               return 0;
+
+       /*
+        * Calculate bucket values, note down max and min values
+        */
+       is_last = false;
+       for (i = 0; i < PLAT_NR && !is_last; i++) {
+               sum += io_u_plat[i];
+               while (sum >= ((long double) plist[j] / 100.0 * nr)) {
+                       assert(plist[j] <= 100.0);
+
+                       ovals[j] = plat_idx_to_val(i);
+                       if (ovals[j] < *minv)
+                               *minv = ovals[j];
+                       if (ovals[j] > *maxv)
+                               *maxv = ovals[j];
+
+                       is_last = (j == len - 1) != 0;
+                       if (is_last)
+                               break;
+
+                       j++;
+               }
+       }
+
+       if (!is_last)
+               fprintf(stderr, "error calculating latency percentiles\n");
+
+       *output = ovals;
+       return len;
+}
+
+static void show_clat_percentiles(unsigned long *io_u_plat, unsigned long nr,
+                                 unsigned int precision)
+{
+       unsigned int divisor, len, i, j = 0;
+       unsigned long minv, maxv;
+       unsigned long *ovals;
+       int per_line, scale_down, time_width;
+       bool is_last;
+       char fmt[32];
+
+       len = calc_clat_percentiles(io_u_plat, nr, &ovals, &maxv, &minv);
+       if (!len || !ovals)
+               goto out;
+
+       if (!tsc_rate) {
+               scale_down = 0;
+               divisor = 1;
+               printf("    percentiles (tsc ticks):\n     |");
+       } else if (minv > 2000 && maxv > 99999) {
+               scale_down = 1;
+               divisor = 1000;
+               printf("    percentiles (usec):\n     |");
+       } else {
+               scale_down = 0;
+               divisor = 1;
+               printf("    percentiles (nsec):\n     |");
+       }
+
+       time_width = max(5, (int) (log10(maxv / divisor) + 1));
+       snprintf(fmt, sizeof(fmt), " %%%u.%ufth=[%%%dllu]%%c", precision + 3,
+                       precision, time_width);
+       /* fmt will be something like " %5.2fth=[%4llu]%c" */
+       per_line = (80 - 7) / (precision + 10 + time_width);
+
+       for (j = 0; j < len; j++) {
+               /* for formatting */
+               if (j != 0 && (j % per_line) == 0)
+                       printf("     |");
+
+               /* end of the list */
+               is_last = (j == len - 1) != 0;
+
+               for (i = 0; i < scale_down; i++)
+                       ovals[j] = (ovals[j] + 999) / 1000;
+
+               printf(fmt, plist[j], ovals[j], is_last ? '\n' : ',');
+
+               if (is_last)
+                       break;
+
+               if ((j % per_line) == per_line - 1)     /* for formatting */
+                       printf("\n");
+       }
+
+out:
+       free(ovals);
+}
+
+static unsigned int plat_val_to_idx(unsigned long val)
+{
+       unsigned int msb, error_bits, base, offset, idx;
+
+       /* Find MSB starting from bit 0 */
+       if (val == 0)
+               msb = 0;
+       else
+               msb = (sizeof(val)*8) - __builtin_clzll(val) - 1;
+
+       /*
+        * MSB <= (PLAT_BITS-1), cannot be rounded off. Use
+        * all bits of the sample as index
+        */
+       if (msb <= PLAT_BITS)
+               return val;
+
+       /* Compute the number of error bits to discard*/
+       error_bits = msb - PLAT_BITS;
+
+       /* Compute the number of buckets before the group */
+       base = (error_bits + 1) << PLAT_BITS;
+
+       /*
+        * Discard the error bits and apply the mask to find the
+        * index for the buckets in the group
+        */
+       offset = (PLAT_VAL - 1) & (val >> error_bits);
+
+       /* Make sure the index does not exceed (array size - 1) */
+       idx = (base + offset) < (PLAT_NR - 1) ?
+               (base + offset) : (PLAT_NR - 1);
+
+       return idx;
+}
+
+static void add_stat(struct submitter *s, int clock_index, int nr)
+{
+#ifdef ARCH_HAVE_CPU_CLOCK
+       unsigned long cycles;
+       unsigned int pidx;
+
+       cycles = get_cpu_clock();
+       cycles -= s->clock_batch[clock_index];
+       pidx = plat_val_to_idx(cycles);
+       s->plat[pidx] += nr;
+#endif
+}
 
 static int io_uring_register_buffers(struct submitter *s)
 {
        if (do_nop)
                return 0;
 
-       return syscall(__NR_sys_io_uring_register, s->ring_fd,
-                       IORING_REGISTER_BUFFERS, s->iovecs, DEPTH);
+       return syscall(__NR_io_uring_register, s->ring_fd,
+                       IORING_REGISTER_BUFFERS, s->iovecs, depth);
 }
 
 static int io_uring_register_files(struct submitter *s)
@@ -116,30 +325,55 @@ static int io_uring_register_files(struct submitter *s)
                s->files[i].fixed_fd = i;
        }
 
-       return syscall(__NR_sys_io_uring_register, s->ring_fd,
+       return syscall(__NR_io_uring_register, s->ring_fd,
                        IORING_REGISTER_FILES, s->fds, s->nr_files);
 }
 
 static int io_uring_setup(unsigned entries, struct io_uring_params *p)
 {
-       return syscall(__NR_sys_io_uring_setup, entries, p);
+       return syscall(__NR_io_uring_setup, entries, p);
+}
+
+static void io_uring_probe(int fd)
+{
+       struct io_uring_probe *p;
+       int ret;
+
+       p = malloc(sizeof(*p) + 256 * sizeof(struct io_uring_probe_op));
+       if (!p)
+               return;
+
+       memset(p, 0, sizeof(*p) + 256 * sizeof(struct io_uring_probe_op));
+       ret = syscall(__NR_io_uring_register, fd, IORING_REGISTER_PROBE, p, 256);
+       if (ret < 0)
+               goto out;
+
+       if (IORING_OP_READ > p->ops_len)
+               goto out;
+
+       if ((p->ops[IORING_OP_READ].flags & IO_URING_OP_SUPPORTED))
+               vectored = 0;
+out:
+       free(p);
 }
 
 static int io_uring_enter(struct submitter *s, unsigned int to_submit,
                          unsigned int min_complete, unsigned int flags)
 {
-       return syscall(__NR_sys_io_uring_enter, s->ring_fd, to_submit,
-                       min_complete, flags);
+       return syscall(__NR_io_uring_enter, s->ring_fd, to_submit, min_complete,
+                       flags, NULL, 0);
 }
 
+#ifndef CONFIG_HAVE_GETTID
 static int gettid(void)
 {
        return syscall(__NR_gettid);
 }
+#endif
 
 static unsigned file_depth(struct submitter *s)
 {
-       return (DEPTH + s->nr_files - 1) / s->nr_files;
+       return (depth + s->nr_files - 1) / s->nr_files;
 }
 
 static void init_io(struct submitter *s, unsigned index)
@@ -167,8 +401,8 @@ static void init_io(struct submitter *s, unsigned index)
        }
        f->pending_ios++;
 
-       lrand48_r(&s->rand, &r);
-       offset = (r % (f->max_blocks - 1)) * BS;
+       r = lrand48();
+       offset = (r % (f->max_blocks - 1)) * bs;
 
        if (register_files) {
                sqe->flags = IOSQE_FIXED_FILE;
@@ -180,8 +414,13 @@ static void init_io(struct submitter *s, unsigned index)
        if (fixedbufs) {
                sqe->opcode = IORING_OP_READ_FIXED;
                sqe->addr = (unsigned long) s->iovecs[index].iov_base;
-               sqe->len = BS;
+               sqe->len = bs;
                sqe->buf_index = index;
+       } else if (!vectored) {
+               sqe->opcode = IORING_OP_READ;
+               sqe->addr = (unsigned long) s->iovecs[index].iov_base;
+               sqe->len = bs;
+               sqe->buf_index = 0;
        } else {
                sqe->opcode = IORING_OP_READV;
                sqe->addr = (unsigned long) &s->iovecs[index];
@@ -190,7 +429,9 @@ static void init_io(struct submitter *s, unsigned index)
        }
        sqe->ioprio = 0;
        sqe->off = offset;
-       sqe->user_data = (unsigned long) f;
+       sqe->user_data = (unsigned long) f->fileno;
+       if (stats)
+               sqe->user_data |= ((unsigned long)s->clock_index << 32);
 }
 
 static int prep_more_ios(struct submitter *s, int max_ios)
@@ -201,8 +442,7 @@ static int prep_more_ios(struct submitter *s, int max_ios)
        next_tail = tail = *ring->tail;
        do {
                next_tail++;
-               read_barrier();
-               if (next_tail == *ring->head)
+               if (next_tail == atomic_load_acquire(ring->head))
                        break;
 
                index = tail & sq_ring_mask;
@@ -212,12 +452,8 @@ static int prep_more_ios(struct submitter *s, int max_ios)
                tail = next_tail;
        } while (prepped < max_ios);
 
-       if (*ring->tail != tail) {
-               /* order tail store with writes to sqes above */
-               write_barrier();
-               *ring->tail = tail;
-               write_barrier();
-       }
+       if (prepped)
+               atomic_store_release(ring->tail, tail);
        return prepped;
 }
 
@@ -233,10 +469,10 @@ static int get_file_size(struct file *f)
                if (ioctl(f->real_fd, BLKGETSIZE64, &bytes) != 0)
                        return -1;
 
-               f->max_blocks = bytes / BS;
+               f->max_blocks = bytes / bs;
                return 0;
        } else if (S_ISREG(st.st_mode)) {
-               f->max_blocks = st.st_size / BS;
+               f->max_blocks = st.st_size / bs;
                return 0;
        }
 
@@ -248,34 +484,52 @@ static int reap_events(struct submitter *s)
        struct io_cq_ring *ring = &s->cq_ring;
        struct io_uring_cqe *cqe;
        unsigned head, reaped = 0;
+       int last_idx = -1, stat_nr = 0;
 
        head = *ring->head;
        do {
                struct file *f;
 
                read_barrier();
-               if (head == *ring->tail)
+               if (head == atomic_load_acquire(ring->tail))
                        break;
                cqe = &ring->cqes[head & cq_ring_mask];
                if (!do_nop) {
-                       f = (struct file *) (uintptr_t) cqe->user_data;
+                       int fileno = cqe->user_data & 0xffffffff;
+
+                       f = &s->files[fileno];
                        f->pending_ios--;
-                       if (cqe->res != BS) {
+                       if (cqe->res != bs) {
                                printf("io: unexpected ret=%d\n", cqe->res);
+                               if (polled && cqe->res == -EOPNOTSUPP)
+                                       printf("Your filesystem/driver/kernel doesn't support polled IO\n");
                                return -1;
                        }
                }
-               if (cqe->flags & IOCQE_FLAG_CACHEHIT)
-                       s->cachehit++;
-               else
-                       s->cachemiss++;
+               if (stats) {
+                       int clock_index = cqe->user_data >> 32;
+
+                       if (last_idx != clock_index) {
+                               if (last_idx != -1) {
+                                       add_stat(s, last_idx, stat_nr);
+                                       stat_nr = 0;
+                               }
+                               last_idx = clock_index;
+                       }
+                       stat_nr++;
+                       add_stat(s, clock_index, 1);
+               }
                reaped++;
                head++;
        } while (1);
 
-       s->inflight -= reaped;
-       *ring->head = head;
-       write_barrier();
+       if (stat_nr)
+               add_stat(s, last_idx, stat_nr);
+
+       if (reaped) {
+               s->inflight -= reaped;
+               atomic_store_release(ring->head, head);
+       }
        return reaped;
 }
 
@@ -283,42 +537,70 @@ static void *submitter_fn(void *data)
 {
        struct submitter *s = data;
        struct io_sq_ring *ring = &s->sq_ring;
-       int ret, prepped;
+       int i, ret, prepped, nr_batch;
+
+       s->tid = gettid();
+       printf("submitter=%d\n", s->tid);
+
+       srand48(pthread_self());
 
-       printf("submitter=%d\n", gettid());
+       for (i = 0; i < MAX_FDS; i++)
+               s->files[i].fileno = i;
 
-       srand48_r(pthread_self(), &s->rand);
+       if (stats) {
+               nr_batch = roundup_pow2(depth / batch_submit);
+               s->clock_batch = calloc(nr_batch, sizeof(unsigned long));
+               s->clock_index = 0;
+
+               s->plat = calloc(PLAT_NR, sizeof(unsigned long));
+       } else {
+               s->clock_batch = NULL;
+               s->plat = NULL;
+               nr_batch = 0;
+       }
 
        prepped = 0;
        do {
                int to_wait, to_submit, this_reap, to_prep;
+               unsigned ring_flags = 0;
 
-               if (!prepped && s->inflight < DEPTH) {
-                       to_prep = min(DEPTH - s->inflight, BATCH_SUBMIT);
+               if (!prepped && s->inflight < depth) {
+                       to_prep = min(depth - s->inflight, batch_submit);
                        prepped = prep_more_ios(s, to_prep);
+#ifdef ARCH_HAVE_CPU_CLOCK
+                       if (prepped && stats) {
+                               s->clock_batch[s->clock_index] = get_cpu_clock();
+                               s->clock_index = (s->clock_index + 1) & (nr_batch - 1);
+                       }
+#endif
                }
                s->inflight += prepped;
 submit_more:
                to_submit = prepped;
 submit:
-               if (to_submit && (s->inflight + to_submit <= DEPTH))
+               if (to_submit && (s->inflight + to_submit <= depth))
                        to_wait = 0;
                else
-                       to_wait = min(s->inflight + to_submit, BATCH_COMPLETE);
+                       to_wait = min(s->inflight + to_submit, batch_complete);
 
                /*
                 * Only need to call io_uring_enter if we're not using SQ thread
                 * poll, or if IORING_SQ_NEED_WAKEUP is set.
                 */
-               if (!sq_thread_poll || (*ring->flags & IORING_SQ_NEED_WAKEUP)) {
+               if (sq_thread_poll)
+                       ring_flags = atomic_load_acquire(ring->flags);
+               if (!sq_thread_poll || ring_flags & IORING_SQ_NEED_WAKEUP) {
                        unsigned flags = 0;
 
                        if (to_wait)
                                flags = IORING_ENTER_GETEVENTS;
-                       if ((*ring->flags & IORING_SQ_NEED_WAKEUP))
+                       if (ring_flags & IORING_SQ_NEED_WAKEUP)
                                flags |= IORING_ENTER_SQ_WAKEUP;
                        ret = io_uring_enter(s, to_submit, to_wait, flags);
                        s->calls++;
+               } else {
+                       /* for SQPOLL, we submitted it all effectively */
+                       ret = to_submit;
                }
 
                /*
@@ -372,10 +654,25 @@ submit:
        return NULL;
 }
 
+static struct submitter *get_submitter(int offset)
+{
+       void *ret;
+
+       ret = submitter;
+       if (offset)
+               ret += offset * (sizeof(*submitter) + depth * sizeof(struct iovec));
+       return ret;
+}
+
 static void sig_int(int sig)
 {
+       int j;
+
        printf("Exiting on signal %d\n", sig);
-       submitters[0].finish = 1;
+       for (j = 0; j < nthreads; j++) {
+               struct submitter *s = get_submitter(j);
+               s->finish = 1;
+       }
        finish = 1;
 }
 
@@ -409,14 +706,23 @@ static int setup_ring(struct submitter *s)
                }
        }
 
-       fd = io_uring_setup(DEPTH, &p);
+       fd = io_uring_setup(depth, &p);
        if (fd < 0) {
                perror("io_uring_setup");
                return 1;
        }
        s->ring_fd = fd;
 
+       io_uring_probe(fd);
+
        if (fixedbufs) {
+               struct rlimit rlim;
+
+               rlim.rlim_cur = RLIM_INFINITY;
+               rlim.rlim_max = RLIM_INFINITY;
+               /* ignore potential error, not needed on newer kernels */
+               setrlimit(RLIMIT_MEMLOCK, &rlim);
+
                ret = io_uring_register_buffers(s);
                if (ret < 0) {
                        perror("io_uring_register_buffers");
@@ -464,122 +770,243 @@ static int setup_ring(struct submitter *s)
 
 static void file_depths(char *buf)
 {
-       struct submitter *s = &submitters[0];
+       bool prev = false;
        char *p;
-       int i;
+       int i, j;
 
        buf[0] = '\0';
        p = buf;
-       for (i = 0; i < s->nr_files; i++) {
-               struct file *f = &s->files[i];
+       for (j = 0; j < nthreads; j++) {
+               struct submitter *s = get_submitter(j);
 
-               if (i + 1 == s->nr_files)
-                       p += sprintf(p, "%d", f->pending_ios);
-               else
-                       p += sprintf(p, "%d, ", f->pending_ios);
+               for (i = 0; i < s->nr_files; i++) {
+                       struct file *f = &s->files[i];
+
+                       if (prev)
+                               p += sprintf(p, " %d", f->pending_ios);
+                       else
+                               p += sprintf(p, "%d", f->pending_ios);
+                       prev = true;
+               }
        }
 }
 
+static void usage(char *argv, int status)
+{
+       printf("%s [options] -- [filenames]\n"
+               " -d <int>  : IO Depth, default %d\n"
+               " -s <int>  : Batch submit, default %d\n"
+               " -c <int>  : Batch complete, default %d\n"
+               " -b <int>  : Block size, default %d\n"
+               " -p <bool> : Polled IO, default %d\n"
+               " -B <bool> : Fixed buffers, default %d\n"
+               " -F <bool> : Register files, default %d\n"
+               " -n <int>  : Number of threads, default %d\n"
+               " -O <bool> : Use O_DIRECT, default %d\n"
+               " -N <bool> : Perform just no-op requests, default %d\n"
+               " -t <bool> : Track IO latencies, default %d\n"
+               " -T <int>  : TSC rate in HZ\n",
+               argv, DEPTH, BATCH_SUBMIT, BATCH_COMPLETE, BS, polled,
+               fixedbufs, register_files, nthreads, !buffered, do_nop, stats);
+       exit(status);
+}
+
 int main(int argc, char *argv[])
 {
-       struct submitter *s = &submitters[0];
-       unsigned long done, calls, reap, cache_hit, cache_miss;
-       int err, i, flags, fd;
+       struct submitter *s;
+       unsigned long done, calls, reap;
+       int err, i, j, flags, fd, opt, threads_per_f, threads_rem = 0, nfiles;
+       struct file f;
        char *fdepths;
        void *ret;
 
-       if (!do_nop && argc < 2) {
-               printf("%s: filename\n", argv[0]);
-               return 1;
+       if (!do_nop && argc < 2)
+               usage(argv[0], 1);
+
+       while ((opt = getopt(argc, argv, "d:s:c:b:p:B:F:n:N:O:t:T:h?")) != -1) {
+               switch (opt) {
+               case 'd':
+                       depth = atoi(optarg);
+                       break;
+               case 's':
+                       batch_submit = atoi(optarg);
+                       if (!batch_submit)
+                               batch_submit = 1;
+                       break;
+               case 'c':
+                       batch_complete = atoi(optarg);
+                       if (!batch_complete)
+                               batch_complete = 1;
+                       break;
+               case 'b':
+                       bs = atoi(optarg);
+                       break;
+               case 'p':
+                       polled = !!atoi(optarg);
+                       break;
+               case 'B':
+                       fixedbufs = !!atoi(optarg);
+                       break;
+               case 'F':
+                       register_files = !!atoi(optarg);
+                       break;
+               case 'n':
+                       nthreads = atoi(optarg);
+                       if (!nthreads) {
+                               printf("Threads must be non-zero\n");
+                               usage(argv[0], 1);
+                       }
+                       break;
+               case 'N':
+                       do_nop = !!atoi(optarg);
+                       break;
+               case 'O':
+                       buffered = !atoi(optarg);
+                       break;
+               case 't':
+#ifndef ARCH_HAVE_CPU_CLOCK
+                       fprintf(stderr, "Stats not supported on this CPU\n");
+                       return 1;
+#endif
+                       stats = !!atoi(optarg);
+                       break;
+               case 'T':
+#ifndef ARCH_HAVE_CPU_CLOCK
+                       fprintf(stderr, "Stats not supported on this CPU\n");
+                       return 1;
+#endif
+                       tsc_rate = strtoul(optarg, NULL, 10);
+                       break;
+               case 'h':
+               case '?':
+               default:
+                       usage(argv[0], 0);
+                       break;
+               }
+       }
+
+       if (batch_complete > depth)
+               batch_complete = depth;
+       if (batch_submit > depth)
+               batch_submit = depth;
+
+       submitter = calloc(nthreads, sizeof(*submitter) +
+                               depth * sizeof(struct iovec));
+       for (j = 0; j < nthreads; j++) {
+               s = get_submitter(j);
+               s->index = j;
+               s->done = s->calls = s->reaps = 0;
        }
 
        flags = O_RDONLY | O_NOATIME;
        if (!buffered)
                flags |= O_DIRECT;
 
-       i = 1;
+       j = 0;
+       i = optind;
+       nfiles = argc - i;
+       if (!do_nop) {
+               if (!nfiles) {
+                       printf("No files specified\n");
+                       usage(argv[0], 1);
+               }
+               threads_per_f = nthreads / nfiles;
+               /* make sure each thread gets assigned files */
+               if (threads_per_f == 0) {
+                       threads_per_f = 1;
+               } else {
+                       threads_rem = nthreads - threads_per_f * nfiles;
+               }
+       }
        while (!do_nop && i < argc) {
-               struct file *f = &s->files[s->nr_files];
+               int k, limit;
+
+               memset(&f, 0, sizeof(f));
 
                fd = open(argv[i], flags);
                if (fd < 0) {
                        perror("open");
                        return 1;
                }
-               f->real_fd = fd;
-               if (get_file_size(f)) {
+               f.real_fd = fd;
+               if (get_file_size(&f)) {
                        printf("failed getting size of device/file\n");
                        return 1;
                }
-               if (f->max_blocks <= 1) {
+               if (f.max_blocks <= 1) {
                        printf("Zero file/device size?\n");
                        return 1;
                }
-               f->max_blocks--;
+               f.max_blocks--;
 
-               printf("Added file %s\n", argv[i]);
-               s->nr_files++;
-               i++;
-       }
+               limit = threads_per_f;
+               limit += threads_rem > 0 ? 1 : 0;
+               for (k = 0; k < limit; k++) {
+                       s = get_submitter((j + k) % nthreads);
 
-       if (fixedbufs) {
-               struct rlimit rlim;
+                       if (s->nr_files == MAX_FDS) {
+                               printf("Max number of files (%d) reached\n", MAX_FDS);
+                               break;
+                       }
 
-               rlim.rlim_cur = RLIM_INFINITY;
-               rlim.rlim_max = RLIM_INFINITY;
-               if (setrlimit(RLIMIT_MEMLOCK, &rlim) < 0) {
-                       perror("setrlimit");
-                       return 1;
+                       memcpy(&s->files[s->nr_files], &f, sizeof(f));
+
+                       printf("Added file %s (submitter %d)\n", argv[i], s->index);
+                       s->nr_files++;
                }
+               threads_rem--;
+               i++;
+               j += limit;
        }
 
        arm_sig_int();
 
-       for (i = 0; i < DEPTH; i++) {
-               void *buf;
+       for (j = 0; j < nthreads; j++) {
+               s = get_submitter(j);
+               for (i = 0; i < depth; i++) {
+                       void *buf;
 
-               if (posix_memalign(&buf, BS, BS)) {
-                       printf("failed alloc\n");
-                       return 1;
+                       if (posix_memalign(&buf, bs, bs)) {
+                               printf("failed alloc\n");
+                               return 1;
+                       }
+                       s->iovecs[i].iov_base = buf;
+                       s->iovecs[i].iov_len = bs;
                }
-               s->iovecs[i].iov_base = buf;
-               s->iovecs[i].iov_len = BS;
        }
 
-       err = setup_ring(s);
-       if (err) {
-               printf("ring setup failed: %s, %d\n", strerror(errno), err);
-               return 1;
+       for (j = 0; j < nthreads; j++) {
+               s = get_submitter(j);
+
+               err = setup_ring(s);
+               if (err) {
+                       printf("ring setup failed: %s, %d\n", strerror(errno), err);
+                       return 1;
+               }
        }
-       printf("polled=%d, fixedbufs=%d, buffered=%d", polled, fixedbufs, buffered);
-       printf(" QD=%d, sq_ring=%d, cq_ring=%d\n", DEPTH, *s->sq_ring.ring_entries, *s->cq_ring.ring_entries);
+       s = get_submitter(0);
+       printf("polled=%d, fixedbufs=%d, register_files=%d, buffered=%d", polled, fixedbufs, register_files, buffered);
+       printf(" QD=%d, sq_ring=%d, cq_ring=%d\n", depth, *s->sq_ring.ring_entries, *s->cq_ring.ring_entries);
 
-       pthread_create(&s->thread, NULL, submitter_fn, s);
+       for (j = 0; j < nthreads; j++) {
+               s = get_submitter(j);
+               pthread_create(&s->thread, NULL, submitter_fn, s);
+       }
 
-       fdepths = malloc(8 * s->nr_files);
-       cache_hit = cache_miss = reap = calls = done = 0;
+       fdepths = malloc(8 * s->nr_files * nthreads);
+       reap = calls = done = 0;
        do {
                unsigned long this_done = 0;
                unsigned long this_reap = 0;
                unsigned long this_call = 0;
-               unsigned long this_cache_hit = 0;
-               unsigned long this_cache_miss = 0;
                unsigned long rpc = 0, ipc = 0;
-               double hit = 0.0;
+               unsigned long iops, bw;
 
                sleep(1);
-               this_done += s->done;
-               this_call += s->calls;
-               this_reap += s->reaps;
-               this_cache_hit += s->cachehit;
-               this_cache_miss += s->cachemiss;
-               if (this_cache_hit && this_cache_miss) {
-                       unsigned long hits, total;
-
-                       hits = this_cache_hit - cache_hit;
-                       total = hits + this_cache_miss - cache_miss;
-                       hit = (double) hits / (double) total;
-                       hit *= 100.0;
+               for (j = 0; j < nthreads; j++) {
+                       this_done += s->done;
+                       this_call += s->calls;
+                       this_reap += s->reaps;
                }
                if (this_call - calls) {
                        rpc = (this_done - done) / (this_call - calls);
@@ -587,18 +1014,38 @@ int main(int argc, char *argv[])
                } else
                        rpc = ipc = -1;
                file_depths(fdepths);
-               printf("IOPS=%lu, IOS/call=%ld/%ld, inflight=%u (%s), Cachehit=%0.2f%%\n",
-                               this_done - done, rpc, ipc, s->inflight,
-                               fdepths, hit);
+               iops = this_done - done;
+               if (bs > 1048576)
+                       bw = iops * (bs / 1048576);
+               else
+                       bw = iops / (1048576 / bs);
+               printf("IOPS=%lu, ", iops);
+               if (!do_nop)
+                       printf("BW=%luMiB/s, ", bw);
+               printf("IOS/call=%ld/%ld, inflight=(%s)\n", rpc, ipc, fdepths);
                done = this_done;
                calls = this_call;
                reap = this_reap;
-               cache_hit = s->cachehit;
-               cache_miss = s->cachemiss;
        } while (!finish);
 
-       pthread_join(s->thread, &ret);
-       close(s->ring_fd);
+       for (j = 0; j < nthreads; j++) {
+               s = get_submitter(j);
+               pthread_join(s->thread, &ret);
+               close(s->ring_fd);
+
+               if (stats) {
+                       unsigned long nr;
+
+                       printf("%d: Latency percentiles:\n", s->tid);
+                       for (i = 0, nr = 0; i < PLAT_NR; i++)
+                               nr += s->plat[i];
+                       show_clat_percentiles(s->plat, nr, 4);
+                       free(s->clock_batch);
+                       free(s->plat);
+               }
+       }
+
        free(fdepths);
+       free(submitter);
        return 0;
 }