options: add category/group to random_generator/random_distribution
[fio.git] / options.c
index 4207537e9c4a4bf4958213b6dd3cf6783582b766..eab22a70d36eec90dc916a6649aa0a8e06639d63 100644 (file)
--- a/options.c
+++ b/options.c
@@ -37,7 +37,7 @@ static int converthexchartoint(char a)
 {
        int base;
 
-       switch(a) {
+       switch (a) {
        case '0'...'9':
                base = '0';
                break;
@@ -50,7 +50,7 @@ static int converthexchartoint(char a)
        default:
                base = 0;
        }
-       return (a - base);
+       return a - base;
 }
 
 static int bs_cmp(const void *p1, const void *p2)
@@ -61,7 +61,7 @@ static int bs_cmp(const void *p1, const void *p2)
        return bsp1->perc < bsp2->perc;
 }
 
-static int bssplit_ddir(struct thread_data *td, int ddir, char *str)
+static int bssplit_ddir(struct thread_options *o, int ddir, char *str)
 {
        struct bssplit *bssplit;
        unsigned int i, perc, perc_missing;
@@ -69,7 +69,7 @@ static int bssplit_ddir(struct thread_data *td, int ddir, char *str)
        long long val;
        char *fname;
 
-       td->o.bssplit_nr[ddir] = 4;
+       o->bssplit_nr[ddir] = 4;
        bssplit = malloc(4 * sizeof(struct bssplit));
 
        i = 0;
@@ -84,9 +84,9 @@ static int bssplit_ddir(struct thread_data *td, int ddir, char *str)
                /*
                 * grow struct buffer, if needed
                 */
-               if (i == td->o.bssplit_nr[ddir]) {
-                       td->o.bssplit_nr[ddir] <<= 1;
-                       bssplit = realloc(bssplit, td->o.bssplit_nr[ddir]
+               if (i == o->bssplit_nr[ddir]) {
+                       o->bssplit_nr[ddir] <<= 1;
+                       bssplit = realloc(bssplit, o->bssplit_nr[ddir]
                                                  * sizeof(struct bssplit));
                }
 
@@ -102,9 +102,9 @@ static int bssplit_ddir(struct thread_data *td, int ddir, char *str)
                } else
                        perc = -1;
 
-               if (str_to_decimal(fname, &val, 1, td)) {
+               if (str_to_decimal(fname, &val, 1, o)) {
                        log_err("fio: bssplit conversion failed\n");
-                       free(td->o.bssplit);
+                       free(o->bssplit);
                        return 1;
                }
 
@@ -118,13 +118,13 @@ static int bssplit_ddir(struct thread_data *td, int ddir, char *str)
                i++;
        }
 
-       td->o.bssplit_nr[ddir] = i;
+       o->bssplit_nr[ddir] = i;
 
        /*
         * Now check if the percentages add up, and how much is missing
         */
        perc = perc_missing = 0;
-       for (i = 0; i < td->o.bssplit_nr[ddir]; i++) {
+       for (i = 0; i < o->bssplit_nr[ddir]; i++) {
                struct bssplit *bsp = &bssplit[i];
 
                if (bsp->perc == (unsigned char) -1)
@@ -143,7 +143,7 @@ static int bssplit_ddir(struct thread_data *td, int ddir, char *str)
         * them.
         */
        if (perc_missing) {
-               for (i = 0; i < td->o.bssplit_nr[ddir]; i++) {
+               for (i = 0; i < o->bssplit_nr[ddir]; i++) {
                        struct bssplit *bsp = &bssplit[i];
 
                        if (bsp->perc == (unsigned char) -1)
@@ -151,22 +151,21 @@ static int bssplit_ddir(struct thread_data *td, int ddir, char *str)
                }
        }
 
-       td->o.min_bs[ddir] = min_bs;
-       td->o.max_bs[ddir] = max_bs;
+       o->min_bs[ddir] = min_bs;
+       o->max_bs[ddir] = max_bs;
 
        /*
         * now sort based on percentages, for ease of lookup
         */
-       qsort(bssplit, td->o.bssplit_nr[ddir], sizeof(struct bssplit), bs_cmp);
-       td->o.bssplit[ddir] = bssplit;
+       qsort(bssplit, o->bssplit_nr[ddir], sizeof(struct bssplit), bs_cmp);
+       o->bssplit[ddir] = bssplit;
        return 0;
-
 }
 
 static int str_bssplit_cb(void *data, const char *input)
 {
        struct thread_data *td = data;
-       char *str, *p, *odir;
+       char *str, *p, *odir, *ddir;
        int ret = 0;
 
        p = str = strdup(input);
@@ -176,78 +175,177 @@ static int str_bssplit_cb(void *data, const char *input)
 
        odir = strchr(str, ',');
        if (odir) {
-               ret = bssplit_ddir(td, DDIR_WRITE, odir + 1);
+               ddir = strchr(odir + 1, ',');
+               if (ddir) {
+                       ret = bssplit_ddir(&td->o, DDIR_TRIM, ddir + 1);
+                       if (!ret)
+                               *ddir = '\0';
+               } else {
+                       char *op;
+
+                       op = strdup(odir + 1);
+                       ret = bssplit_ddir(&td->o, DDIR_TRIM, op);
+
+                       free(op);
+               }
+               if (!ret)
+                       ret = bssplit_ddir(&td->o, DDIR_WRITE, odir + 1);
                if (!ret) {
                        *odir = '\0';
-                       ret = bssplit_ddir(td, DDIR_READ, str);
+                       ret = bssplit_ddir(&td->o, DDIR_READ, str);
                }
        } else {
                char *op;
 
                op = strdup(str);
-
-               ret = bssplit_ddir(td, DDIR_READ, str);
-               if (!ret)
-                       ret = bssplit_ddir(td, DDIR_WRITE, op);
-
+               ret = bssplit_ddir(&td->o, DDIR_WRITE, op);
                free(op);
+
+               if (!ret) {
+                       op = strdup(str);
+                       ret = bssplit_ddir(&td->o, DDIR_TRIM, op);
+                       free(op);
+               }
+               ret = bssplit_ddir(&td->o, DDIR_READ, str);
        }
 
        free(p);
        return ret;
 }
 
+static int str2error(char *str)
+{
+       const char *err[] = { "EPERM", "ENOENT", "ESRCH", "EINTR", "EIO",
+                           "ENXIO", "E2BIG", "ENOEXEC", "EBADF",
+                           "ECHILD", "EAGAIN", "ENOMEM", "EACCES",
+                           "EFAULT", "ENOTBLK", "EBUSY", "EEXIST",
+                           "EXDEV", "ENODEV", "ENOTDIR", "EISDIR",
+                           "EINVAL", "ENFILE", "EMFILE", "ENOTTY",
+                           "ETXTBSY","EFBIG", "ENOSPC", "ESPIPE",
+                           "EROFS","EMLINK", "EPIPE", "EDOM", "ERANGE" };
+       int i = 0, num = sizeof(err) / sizeof(void *);
+
+       while (i < num) {
+               if (!strcmp(err[i], str))
+                       return i + 1;
+               i++;
+       }
+       return 0;
+}
+
+static int ignore_error_type(struct thread_data *td, int etype, char *str)
+{
+       unsigned int i;
+       int *error;
+       char *fname;
+
+       if (etype >= ERROR_TYPE_CNT) {
+               log_err("Illegal error type\n");
+               return 1;
+       }
+
+       td->o.ignore_error_nr[etype] = 4;
+       error = malloc(4 * sizeof(struct bssplit));
+
+       i = 0;
+       while ((fname = strsep(&str, ":")) != NULL) {
+
+               if (!strlen(fname))
+                       break;
+
+               /*
+                * grow struct buffer, if needed
+                */
+               if (i == td->o.ignore_error_nr[etype]) {
+                       td->o.ignore_error_nr[etype] <<= 1;
+                       error = realloc(error, td->o.ignore_error_nr[etype]
+                                                 * sizeof(int));
+               }
+               if (fname[0] == 'E') {
+                       error[i] = str2error(fname);
+               } else {
+                       error[i] = atoi(fname);
+                       if (error[i] < 0)
+                               error[i] = error[i];
+               }
+               if (!error[i]) {
+                       log_err("Unknown error %s, please use number value \n",
+                                 fname);
+                       return 1;
+               }
+               i++;
+       }
+       if (i) {
+               td->o.continue_on_error |= 1 << etype;
+               td->o.ignore_error_nr[etype] = i;
+               td->o.ignore_error[etype] = error;
+       }
+       return 0;
+
+}
+
+static int str_ignore_error_cb(void *data, const char *input)
+{
+       struct thread_data *td = data;
+       char *str, *p, *n;
+       int type = 0, ret = 1;
+       p = str = strdup(input);
+
+       strip_blank_front(&str);
+       strip_blank_end(str);
+
+       while (p) {
+               n = strchr(p, ',');
+               if (n)
+                       *n++ = '\0';
+               ret = ignore_error_type(td, type, p);
+               if (ret)
+                       break;
+               p = n;
+               type++;
+       }
+       free(str);
+       return ret;
+}
+
 static int str_rw_cb(void *data, const char *str)
 {
        struct thread_data *td = data;
+       struct thread_options *o = &td->o;
        char *nr = get_opt_postfix(str);
 
-       td->o.ddir_seq_nr = 1;
-       td->o.ddir_seq_add = 0;
+       o->ddir_seq_nr = 1;
+       o->ddir_seq_add = 0;
 
        if (!nr)
                return 0;
 
        if (td_random(td))
-               td->o.ddir_seq_nr = atoi(nr);
+               o->ddir_seq_nr = atoi(nr);
        else {
                long long val;
 
-               if (str_to_decimal(nr, &val, 1, td)) {
+               if (str_to_decimal(nr, &val, 1, o)) {
                        log_err("fio: rw postfix parsing failed\n");
                        free(nr);
                        return 1;
                }
 
-               td->o.ddir_seq_add = val;
+               o->ddir_seq_add = val;
        }
 
        free(nr);
        return 0;
 }
 
-#ifdef FIO_HAVE_LIBAIO
-static int str_libaio_cb(void *data, const char *str)
-{
-       struct thread_data *td = data;
-
-       if (!strcmp(str, "userspace_reap")) {
-               td->o.userspace_libaio_reap = 1;
-               return 0;
-       }
-
-       log_err("fio: bad libaio sub-option: %s\n", str);
-       return 1;
-}
-#endif
-
 static int str_mem_cb(void *data, const char *mem)
 {
        struct thread_data *td = data;
+       struct thread_options *o = &td->o;
 
-       if (td->o.mem_type == MEM_MMAPHUGE || td->o.mem_type == MEM_MMAP) {
-               td->mmapfile = get_opt_postfix(mem);
-               if (td->o.mem_type == MEM_MMAPHUGE && !td->mmapfile) {
+       if (o->mem_type == MEM_MMAPHUGE || o->mem_type == MEM_MMAP) {
+               o->mmapfile = get_opt_postfix(mem);
+               if (o->mem_type == MEM_MMAPHUGE && !o->mmapfile) {
                        log_err("fio: mmaphuge:/path/to/file\n");
                        return 1;
                }
@@ -256,21 +354,6 @@ static int str_mem_cb(void *data, const char *mem)
        return 0;
 }
 
-static int str_verify_cb(void *data, const char *mem)
-{
-       struct thread_data *td = data;
-
-       if (td->o.verify != VERIFY_CRC32C_INTEL)
-               return 0;
-
-       if (!crc32c_intel_works()) {
-               log_info("fio: System does not support hw accelerated crc32c. Falling back to sw crc32c.\n");
-               td->o.verify = VERIFY_CRC32C;
-       }
-
-       return 0;
-}
-
 static int fio_clock_source_cb(void *data, const char *str)
 {
        struct thread_data *td = data;
@@ -280,12 +363,6 @@ static int fio_clock_source_cb(void *data, const char *str)
        return 0;
 }
 
-static int str_lockmem_cb(void fio_unused *data, unsigned long long *val)
-{
-       mlock_size = *val;
-       return 0;
-}
-
 static int str_rwmix_read_cb(void *data, unsigned long long *val)
 {
        struct thread_data *td = data;
@@ -304,40 +381,6 @@ static int str_rwmix_write_cb(void *data, unsigned long long *val)
        return 0;
 }
 
-#ifdef FIO_HAVE_IOPRIO
-static int str_prioclass_cb(void *data, unsigned long long *val)
-{
-       struct thread_data *td = data;
-       unsigned short mask;
-
-       /*
-        * mask off old class bits, str_prio_cb() may have set a default class
-        */
-       mask = (1 << IOPRIO_CLASS_SHIFT) - 1;
-       td->ioprio &= mask;
-
-       td->ioprio |= *val << IOPRIO_CLASS_SHIFT;
-       td->ioprio_set = 1;
-       return 0;
-}
-
-static int str_prio_cb(void *data, unsigned long long *val)
-{
-       struct thread_data *td = data;
-
-       td->ioprio |= *val;
-
-       /*
-        * If no class is set, assume BE
-        */
-       if ((td->ioprio >> IOPRIO_CLASS_SHIFT) == 0)
-               td->ioprio |= IOPRIO_CLASS_BE << IOPRIO_CLASS_SHIFT;
-
-       td->ioprio_set = 1;
-       return 0;
-}
-#endif
-
 static int str_exitall_cb(void)
 {
        exitall_on_terminate = 1;
@@ -470,14 +513,128 @@ static int str_verify_cpus_allowed_cb(void *data, const char *input)
 }
 #endif
 
-#ifdef FIO_HAVE_TRIM
-static int str_verify_trim_cb(void *data, unsigned long long *val)
+#ifdef FIO_HAVE_LIBNUMA
+static int str_numa_cpunodes_cb(void *data, char *input)
 {
        struct thread_data *td = data;
 
-       td->o.trim_percentage = *val;
+       /* numa_parse_nodestring() parses a character string list
+        * of nodes into a bit mask. The bit mask is allocated by
+        * numa_allocate_nodemask(), so it should be freed by
+        * numa_free_nodemask().
+        */
+       td->o.numa_cpunodesmask = numa_parse_nodestring(input);
+       if (td->o.numa_cpunodesmask == NULL) {
+               log_err("fio: numa_parse_nodestring failed\n");
+               td_verror(td, 1, "str_numa_cpunodes_cb");
+               return 1;
+       }
+
+       td->o.numa_cpumask_set = 1;
        return 0;
 }
+
+static int str_numa_mpol_cb(void *data, char *input)
+{
+       struct thread_data *td = data;
+       const char * const policy_types[] =
+               { "default", "prefer", "bind", "interleave", "local" };
+       int i;
+
+       char *nodelist = strchr(input, ':');
+       if (nodelist) {
+               /* NUL-terminate mode */
+               *nodelist++ = '\0';
+       }
+
+       for (i = 0; i <= MPOL_LOCAL; i++) {
+               if (!strcmp(input, policy_types[i])) {
+                       td->o.numa_mem_mode = i;
+                       break;
+               }
+       }
+       if (i > MPOL_LOCAL) {
+               log_err("fio: memory policy should be: default, prefer, bind, interleave, local\n");
+               goto out;
+       }
+
+       switch (td->o.numa_mem_mode) {
+       case MPOL_PREFERRED:
+               /*
+                * Insist on a nodelist of one node only
+                */
+               if (nodelist) {
+                       char *rest = nodelist;
+                       while (isdigit(*rest))
+                               rest++;
+                       if (*rest) {
+                               log_err("fio: one node only for \'prefer\'\n");
+                               goto out;
+                       }
+               } else {
+                       log_err("fio: one node is needed for \'prefer\'\n");
+                       goto out;
+               }
+               break;
+       case MPOL_INTERLEAVE:
+               /*
+                * Default to online nodes with memory if no nodelist
+                */
+               if (!nodelist)
+                       nodelist = strdup("all");
+               break;
+       case MPOL_LOCAL:
+       case MPOL_DEFAULT:
+               /*
+                * Don't allow a nodelist
+                */
+               if (nodelist) {
+                       log_err("fio: NO nodelist for \'local\'\n");
+                       goto out;
+               }
+               break;
+       case MPOL_BIND:
+               /*
+                * Insist on a nodelist
+                */
+               if (!nodelist) {
+                       log_err("fio: a nodelist is needed for \'bind\'\n");
+                       goto out;
+               }
+               break;
+       }
+
+
+       /* numa_parse_nodestring() parses a character string list
+        * of nodes into a bit mask. The bit mask is allocated by
+        * numa_allocate_nodemask(), so it should be freed by
+        * numa_free_nodemask().
+        */
+       switch (td->o.numa_mem_mode) {
+       case MPOL_PREFERRED:
+               td->o.numa_mem_prefer_node = atoi(nodelist);
+               break;
+       case MPOL_INTERLEAVE:
+       case MPOL_BIND:
+               td->o.numa_memnodesmask = numa_parse_nodestring(nodelist);
+               if (td->o.numa_memnodesmask == NULL) {
+                       log_err("fio: numa_parse_nodestring failed\n");
+                       td_verror(td, 1, "str_numa_memnodes_cb");
+                       return 1;
+               }
+               break;
+       case MPOL_LOCAL:
+       case MPOL_DEFAULT:
+       default:
+               break;
+       }
+
+       td->o.numa_memmask_set = 1;
+       return 0;
+
+out:
+       return 1;
+}
 #endif
 
 static int str_fst_cb(void *data, const char *str)
@@ -510,41 +667,41 @@ static int str_sfr_cb(void *data, const char *str)
 }
 #endif
 
-static int check_dir(struct thread_data *td, char *fname)
+static int str_random_distribution_cb(void *data, const char *str)
 {
-#if 0
-       char file[PATH_MAX], *dir;
-       int elen = 0;
-
-       if (td->o.directory) {
-               strcpy(file, td->o.directory);
-               strcat(file, "/");
-               elen = strlen(file);
-       }
-
-       sprintf(file + elen, "%s", fname);
-       dir = dirname(file);
+       struct thread_data *td = data;
+       double val;
+       char *nr;
 
-       {
-       struct stat sb;
-       /*
-        * We can't do this on FIO_DISKLESSIO engines. The engine isn't loaded
-        * yet, so we can't do this check right here...
-        */
-       if (lstat(dir, &sb) < 0) {
-               int ret = errno;
+       if (td->o.random_distribution == FIO_RAND_DIST_ZIPF)
+               val = 1.1;
+       else if (td->o.random_distribution == FIO_RAND_DIST_PARETO)
+               val = 0.2;
+       else
+               return 0;
 
-               log_err("fio: %s is not a directory\n", dir);
-               td_verror(td, ret, "lstat");
+       nr = get_opt_postfix(str);
+       if (nr && !str_to_float(nr, &val)) {
+               log_err("fio: random postfix parsing failed\n");
+               free(nr);
                return 1;
        }
 
-       if (!S_ISDIR(sb.st_mode)) {
-               log_err("fio: %s is not a directory\n", dir);
-               return 1;
-       }
+       free(nr);
+
+       if (td->o.random_distribution == FIO_RAND_DIST_ZIPF) {
+               if (val == 1.00) {
+                       log_err("fio: zipf theta must different than 1.0\n");
+                       return 1;
+               }
+               td->o.zipf_theta.u.f = val;
+       } else {
+               if (val <= 0.00 || val >= 1.00) {
+                       log_err("fio: pareto input out of range (0 < input < 1.0)\n");
+                       return 1;
+               }
+               td->o.pareto_h.u.f = val;
        }
-#endif
 
        return 0;
 }
@@ -595,14 +752,6 @@ static char *get_next_file_name(char **ptr)
        return start;
 }
 
-static int str_hostname_cb(void *data, const char *input)
-{
-       struct thread_data *td = data;
-
-       td->o.filename = strdup(input);
-       return 0;
-}
-
 static int str_filename_cb(void *data, const char *input)
 {
        struct thread_data *td = data;
@@ -619,10 +768,6 @@ static int str_filename_cb(void *data, const char *input)
        while ((fname = get_next_file_name(&str)) != NULL) {
                if (!strlen(fname))
                        break;
-               if (check_dir(td, fname)) {
-                       free(p);
-                       return 1;
-               }
                add_file(td, fname);
                td->o.nr_files++;
        }
@@ -661,25 +806,12 @@ static int str_opendir_cb(void *data, const char fio_unused *str)
        return add_dir_files(td, td->o.opendir);
 }
 
-static int str_verify_offset_cb(void *data, unsigned long long *off)
-{
-       struct thread_data *td = data;
-
-       if (*off && *off < sizeof(struct verify_header)) {
-               log_err("fio: verify_offset too small\n");
-               return 1;
-       }
-
-       td->o.verify_offset = *off;
-       return 0;
-}
-
 static int str_verify_pattern_cb(void *data, const char *input)
 {
        struct thread_data *td = data;
        long off;
        int i = 0, j = 0, len, k, base = 10;
-       char* loc1, * loc2;
+       char *loc1, *loc2;
 
        loc1 = strstr(input, "0x");
        loc2 = strstr(input, "0X");
@@ -712,12 +844,32 @@ static int str_verify_pattern_cb(void *data, const char *input)
                        }
                }
        }
+
+       /*
+        * Fill the pattern all the way to the end. This greatly reduces
+        * the number of memcpy's we have to do when verifying the IO.
+        */
+       while (i > 1 && i * 2 <= MAX_PATTERN_SIZE) {
+               memcpy(&td->o.verify_pattern[i], &td->o.verify_pattern[0], i);
+               i *= 2;
+       }
+       if (i == 1) {
+               /*
+                * The code in verify_io_u_pattern assumes a single byte pattern
+                * fills the whole verify pattern buffer.
+                */
+               memset(td->o.verify_pattern, td->o.verify_pattern[0],
+                      MAX_PATTERN_SIZE);
+       }
+
        td->o.verify_pattern_bytes = i;
+
        /*
         * VERIFY_META could already be set
         */
        if (td->o.verify == VERIFY_NONE)
                td->o.verify = VERIFY_PATTERN;
+
        return 0;
 }
 
@@ -735,39 +887,6 @@ static int str_lockfile_cb(void *data, const char *str)
        return 0;
 }
 
-static int str_write_bw_log_cb(void *data, const char *str)
-{
-       struct thread_data *td = data;
-
-       if (str)
-               td->o.bw_log_file = strdup(str);
-
-       td->o.write_bw_log = 1;
-       return 0;
-}
-
-static int str_write_lat_log_cb(void *data, const char *str)
-{
-       struct thread_data *td = data;
-
-       if (str)
-               td->o.lat_log_file = strdup(str);
-
-       td->o.write_lat_log = 1;
-       return 0;
-}
-
-static int str_write_iops_log_cb(void *data, const char *str)
-{
-       struct thread_data *td = data;
-
-       if (str)
-               td->o.iops_log_file = strdup(str);
-
-       td->o.write_iops_log = 1;
-       return 0;
-}
-
 static int str_gtod_reduce_cb(void *data, int *il)
 {
        struct thread_data *td = data;
@@ -849,60 +968,216 @@ static int kb_base_verify(struct fio_option *o, void *data)
        return 0;
 }
 
+/*
+ * Option grouping
+ */
+static struct opt_group fio_opt_groups[] = {
+       {
+               .name   = "General",
+               .mask   = FIO_OPT_C_GENERAL,
+       },
+       {
+               .name   = "I/O",
+               .mask   = FIO_OPT_C_IO,
+       },
+       {
+               .name   = "File",
+               .mask   = FIO_OPT_C_FILE,
+       },
+       {
+               .name   = "Statistics",
+               .mask   = FIO_OPT_C_STAT,
+       },
+       {
+               .name   = "Logging",
+               .mask   = FIO_OPT_C_LOG,
+       },
+       {
+               .name   = "Profiles",
+               .mask   = FIO_OPT_C_PROFILE,
+       },
+       {
+               .name   = NULL,
+       },
+};
+
+static struct opt_group *__opt_group_from_mask(struct opt_group *ogs, unsigned int *mask,
+                                              unsigned int inv_mask)
+{
+       struct opt_group *og;
+       int i;
+
+       if (*mask == inv_mask || !*mask)
+               return NULL;
+
+       for (i = 0; ogs[i].name; i++) {
+               og = &ogs[i];
+
+               if (*mask & og->mask) {
+                       *mask &= ~(og->mask);
+                       return og;
+               }
+       }
+
+       return NULL;
+}
+
+struct opt_group *opt_group_from_mask(unsigned int *mask)
+{
+       return __opt_group_from_mask(fio_opt_groups, mask, FIO_OPT_C_INVALID);
+}
+
+static struct opt_group fio_opt_cat_groups[] = {
+       {
+               .name   = "Rate",
+               .mask   = FIO_OPT_G_RATE,
+       },
+       {
+               .name   = "Zone",
+               .mask   = FIO_OPT_G_ZONE,
+       },
+       {
+               .name   = "Read/write mix",
+               .mask   = FIO_OPT_G_RWMIX,
+       },
+       {
+               .name   = "Verify",
+               .mask   = FIO_OPT_G_VERIFY,
+       },
+       {
+               .name   = "Trim",
+               .mask   = FIO_OPT_G_TRIM,
+       },
+       {
+               .name   = "I/O Logging",
+               .mask   = FIO_OPT_G_IOLOG,
+       },
+       {
+               .name   = "I/O Depth",
+               .mask   = FIO_OPT_G_IO_DEPTH,
+       },
+       {
+               .name   = "I/O Flow",
+               .mask   = FIO_OPT_G_IO_FLOW,
+       },
+       {
+               .name   = "Description",
+               .mask   = FIO_OPT_G_DESC,
+       },
+       {
+               .name   = "Filename",
+               .mask   = FIO_OPT_G_FILENAME,
+       },
+       {
+               .name   = "General I/O",
+               .mask   = FIO_OPT_G_IO_BASIC,
+       },
+       {
+               .name   = "Cgroups",
+               .mask   = FIO_OPT_G_CGROUP,
+       },
+       {
+               .name   = "Runtime",
+               .mask   = FIO_OPT_G_RUNTIME,
+       },
+       {
+               .name   = "Process",
+               .mask   = FIO_OPT_G_PROCESS,
+       },
+       {
+               .name   = "Job credentials / priority",
+               .mask   = FIO_OPT_G_CRED,
+       },
+       {
+               .name   = "Clock settings",
+               .mask   = FIO_OPT_G_CLOCK,
+       },
+       {
+               .name   = "I/O Type",
+               .mask   = FIO_OPT_G_IO_TYPE,
+       },
+       {
+               .name   = "I/O Thinktime",
+               .mask   = FIO_OPT_G_THINKTIME,
+       },
+       {
+               .name   = "Randomizations",
+               .mask   = FIO_OPT_G_RANDOM,
+       },
+       {
+               .name   = "I/O buffers",
+               .mask   = FIO_OPT_G_IO_BUF,
+       },
+       {
+               .name   = "Tiobench profile",
+               .mask   = FIO_OPT_G_TIOBENCH,
+       },
+
+       {
+               .name   = NULL,
+       }
+};
+
+struct opt_group *opt_group_cat_from_mask(unsigned int *mask)
+{
+       return __opt_group_from_mask(fio_opt_cat_groups, mask, FIO_OPT_G_INVALID);
+}
+
 /*
  * Map of job/command line options
  */
-static struct fio_option options[FIO_MAX_OPTS] = {
+struct fio_option fio_options[FIO_MAX_OPTS] = {
        {
                .name   = "description",
+               .lname  = "Description of job",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(description),
                .help   = "Text job description",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_DESC,
        },
        {
                .name   = "name",
+               .lname  = "Job name",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(name),
                .help   = "Name of this job",
-       },
-       {
-               .name   = "directory",
-               .type   = FIO_OPT_STR_STORE,
-               .off1   = td_var_offset(directory),
-               .cb     = str_directory_cb,
-               .help   = "Directory to store files in",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_DESC,
        },
        {
                .name   = "filename",
+               .lname  = "Filename(s)",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(filename),
                .cb     = str_filename_cb,
                .prio   = -1, /* must come after "directory" */
                .help   = "File(s) to use for the workload",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_FILENAME,
        },
        {
-               .name   = "hostname",
+               .name   = "directory",
+               .lname  = "Directory",
                .type   = FIO_OPT_STR_STORE,
-               .cb     = str_hostname_cb,
-               .help   = "Hostname for net IO engine",
-       },
-       {
-               .name   = "kb_base",
-               .type   = FIO_OPT_INT,
-               .off1   = td_var_offset(kb_base),
-               .verify = kb_base_verify,
-               .prio   = 1,
-               .def    = "1024",
-               .help   = "How many bytes per KB for reporting (1000 or 1024)",
+               .off1   = td_var_offset(directory),
+               .cb     = str_directory_cb,
+               .help   = "Directory to store files in",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_FILENAME,
        },
        {
                .name   = "lockfile",
+               .lname  = "Lockfile",
                .type   = FIO_OPT_STR,
                .cb     = str_lockfile_cb,
                .off1   = td_var_offset(file_lock_mode),
                .help   = "Lock file when doing IO to it",
                .parent = "filename",
+               .hide   = 0,
                .def    = "none",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_FILENAME,
                .posval = {
                          { .ival = "none",
                            .oval = FILE_LOCK_NONE,
@@ -921,13 +1196,17 @@ static struct fio_option options[FIO_MAX_OPTS] = {
        },
        {
                .name   = "opendir",
+               .lname  = "Open directory",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(opendir),
                .cb     = str_opendir_cb,
                .help   = "Recursively add files from this directory and down",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_FILENAME,
        },
        {
                .name   = "rw",
+               .lname  = "Read/write",
                .alias  = "readwrite",
                .type   = FIO_OPT_STR,
                .cb     = str_rw_cb,
@@ -935,6 +1214,8 @@ static struct fio_option options[FIO_MAX_OPTS] = {
                .help   = "IO direction",
                .def    = "read",
                .verify = rw_verify,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_BASIC,
                .posval = {
                          { .ival = "read",
                            .oval = TD_DDIR_READ,
@@ -944,6 +1225,10 @@ static struct fio_option options[FIO_MAX_OPTS] = {
                            .oval = TD_DDIR_WRITE,
                            .help = "Sequential write",
                          },
+                         { .ival = "trim",
+                           .oval = TD_DDIR_TRIM,
+                           .help = "Sequential trim",
+                         },
                          { .ival = "randread",
                            .oval = TD_DDIR_RANDREAD,
                            .help = "Random read",
@@ -952,10 +1237,18 @@ static struct fio_option options[FIO_MAX_OPTS] = {
                            .oval = TD_DDIR_RANDWRITE,
                            .help = "Random write",
                          },
+                         { .ival = "randtrim",
+                           .oval = TD_DDIR_RANDTRIM,
+                           .help = "Random trim",
+                         },
                          { .ival = "rw",
                            .oval = TD_DDIR_RW,
                            .help = "Sequential read and write mix",
                          },
+                         { .ival = "readwrite",
+                           .oval = TD_DDIR_RW,
+                           .help = "Sequential read and write mix",
+                         },
                          { .ival = "randrw",
                            .oval = TD_DDIR_RANDRW,
                            .help = "Random read and write mix"
@@ -964,10 +1257,13 @@ static struct fio_option options[FIO_MAX_OPTS] = {
        },
        {
                .name   = "rw_sequencer",
+               .lname  = "RW Sequencer",
                .type   = FIO_OPT_STR,
                .off1   = td_var_offset(rw_seq),
                .help   = "IO offset generator modifier",
                .def    = "sequential",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_BASIC,
                .posval = {
                          { .ival = "sequential",
                            .oval = RW_SEQ_SEQ,
@@ -982,10 +1278,13 @@ static struct fio_option options[FIO_MAX_OPTS] = {
 
        {
                .name   = "ioengine",
+               .lname  = "IO Engine",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(ioengine),
                .help   = "IO engine to use",
                .def    = FIO_PREFERRED_ENGINE,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_BASIC,
                .posval = {
                          { .ival = "sync",
                            .help = "Use read/write",
@@ -999,7 +1298,6 @@ static struct fio_option options[FIO_MAX_OPTS] = {
 #ifdef FIO_HAVE_LIBAIO
                          { .ival = "libaio",
                            .help = "Linux native asynchronous IO",
-                           .cb   = str_libaio_cb,
                          },
 #endif
 #ifdef FIO_HAVE_POSIXAIO
@@ -1015,7 +1313,7 @@ static struct fio_option options[FIO_MAX_OPTS] = {
 #ifdef FIO_HAVE_WINDOWSAIO
                          { .ival = "windowsaio",
                            .help = "Windows native asynchronous IO"
-                         },
+                         },
 #endif
                          { .ival = "mmap",
                            .help = "Memory mapped IO"
@@ -1061,6 +1359,21 @@ static struct fio_option options[FIO_MAX_OPTS] = {
                          { .ival = "rdma",
                            .help = "RDMA IO engine",
                          },
+#endif
+#ifdef FIO_HAVE_FUSION_AW
+                         { .ival = "fusion-aw-sync",
+                           .help = "Fusion-io atomic write engine",
+                         },
+#endif
+#ifdef FIO_HAVE_E4_ENG
+                         { .ival = "e4defrag",
+                           .help = "ext4 defrag engine",
+                         },
+#endif
+#ifdef FIO_HAVE_FALLOC_ENG
+                         { .ival = "falloc",
+                           .help = "fallocate() file based engine",
+                         },
 #endif
                          { .ival = "external",
                            .help = "Load external engine (append name)",
@@ -1069,168 +1382,312 @@ static struct fio_option options[FIO_MAX_OPTS] = {
        },
        {
                .name   = "iodepth",
+               .lname  = "IO Depth",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(iodepth),
                .help   = "Number of IO buffers to keep in flight",
                .minval = 1,
+               .interval = 1,
                .def    = "1",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_BASIC,
        },
        {
                .name   = "iodepth_batch",
+               .lname  = "IO Depth batch",
                .alias  = "iodepth_batch_submit",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(iodepth_batch),
                .help   = "Number of IO buffers to submit in one go",
                .parent = "iodepth",
+               .hide   = 1,
                .minval = 1,
+               .interval = 1,
                .def    = "1",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_BASIC,
        },
        {
                .name   = "iodepth_batch_complete",
+               .lname  = "IO Depth batch complete",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(iodepth_batch_complete),
                .help   = "Number of IO buffers to retrieve in one go",
                .parent = "iodepth",
+               .hide   = 1,
                .minval = 0,
+               .interval = 1,
                .def    = "1",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_BASIC,
        },
        {
                .name   = "iodepth_low",
+               .lname  = "IO Depth batch low",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(iodepth_low),
                .help   = "Low water mark for queuing depth",
                .parent = "iodepth",
+               .hide   = 1,
+               .interval = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_BASIC,
        },
        {
                .name   = "size",
+               .lname  = "Size",
                .type   = FIO_OPT_STR_VAL,
                .cb     = str_size_cb,
                .help   = "Total size of device or files",
+               .interval = 1024 * 1024,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "fill_device",
+               .lname  = "Fill device",
                .alias  = "fill_fs",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(fill_device),
                .help   = "Write until an ENOSPC error occurs",
                .def    = "0",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "filesize",
+               .lname  = "File size",
                .type   = FIO_OPT_STR_VAL,
                .off1   = td_var_offset(file_size_low),
                .off2   = td_var_offset(file_size_high),
                .minval = 1,
                .help   = "Size of individual files",
+               .interval = 1024 * 1024,
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "offset",
+               .lname  = "IO offset",
                .alias  = "fileoffset",
                .type   = FIO_OPT_STR_VAL,
                .off1   = td_var_offset(start_offset),
                .help   = "Start IO from this offset",
                .def    = "0",
+               .interval = 1024 * 1024,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_INVALID,
+       },
+       {
+               .name   = "offset_increment",
+               .lname  = "IO offset increment",
+               .type   = FIO_OPT_STR_VAL,
+               .off1   = td_var_offset(offset_increment),
+               .help   = "What is the increment from one offset to the next",
+               .parent = "offset",
+               .hide   = 1,
+               .def    = "0",
+               .interval = 1024 * 1024,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "bs",
+               .lname  = "Block size",
                .alias  = "blocksize",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(bs[DDIR_READ]),
                .off2   = td_var_offset(bs[DDIR_WRITE]),
+               .off3   = td_var_offset(bs[DDIR_TRIM]),
                .minval = 1,
                .help   = "Block size unit",
                .def    = "4k",
                .parent = "rw",
+               .hide   = 1,
+               .interval = 512,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "ba",
+               .lname  = "Block size align",
                .alias  = "blockalign",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(ba[DDIR_READ]),
                .off2   = td_var_offset(ba[DDIR_WRITE]),
+               .off3   = td_var_offset(ba[DDIR_TRIM]),
                .minval = 1,
                .help   = "IO block offset alignment",
                .parent = "rw",
+               .hide   = 1,
+               .interval = 512,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "bsrange",
+               .lname  = "Block size range",
                .alias  = "blocksize_range",
                .type   = FIO_OPT_RANGE,
                .off1   = td_var_offset(min_bs[DDIR_READ]),
                .off2   = td_var_offset(max_bs[DDIR_READ]),
                .off3   = td_var_offset(min_bs[DDIR_WRITE]),
                .off4   = td_var_offset(max_bs[DDIR_WRITE]),
+               .off5   = td_var_offset(min_bs[DDIR_TRIM]),
+               .off6   = td_var_offset(max_bs[DDIR_TRIM]),
                .minval = 1,
                .help   = "Set block size range (in more detail than bs)",
                .parent = "rw",
+               .hide   = 1,
+               .interval = 4096,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "bssplit",
+               .lname  = "Block size split",
                .type   = FIO_OPT_STR,
                .cb     = str_bssplit_cb,
                .help   = "Set a specific mix of block sizes",
                .parent = "rw",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "bs_unaligned",
+               .lname  = "Block size unaligned",
                .alias  = "blocksize_unaligned",
                .type   = FIO_OPT_STR_SET,
                .off1   = td_var_offset(bs_unaligned),
                .help   = "Don't sector align IO buffer sizes",
                .parent = "rw",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "randrepeat",
+               .lname  = "Random repeatable",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(rand_repeatable),
                .help   = "Use repeatable random IO pattern",
                .def    = "1",
                .parent = "rw",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RANDOM,
        },
        {
                .name   = "use_os_rand",
+               .lname  = "Use OS random",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(use_os_rand),
                .help   = "Set to use OS random generator",
                .def    = "0",
                .parent = "rw",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RANDOM,
        },
        {
                .name   = "norandommap",
+               .lname  = "No randommap",
                .type   = FIO_OPT_STR_SET,
                .off1   = td_var_offset(norandommap),
                .help   = "Accept potential duplicate random blocks",
                .parent = "rw",
+               .hide   = 1,
+               .hide_on_set = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RANDOM,
        },
        {
                .name   = "softrandommap",
+               .lname  = "Soft randommap",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(softrandommap),
                .help   = "Set norandommap if randommap allocation fails",
                .parent = "norandommap",
+               .hide   = 1,
                .def    = "0",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RANDOM,
+       },
+       {
+               .name   = "random_generator",
+               .type   = FIO_OPT_STR,
+               .off1   = td_var_offset(random_generator),
+               .help   = "Type of random number generator to use",
+               .def    = "tausworthe",
+               .posval = {
+                         { .ival = "tausworthe",
+                           .oval = FIO_RAND_GEN_TAUSWORTHE,
+                           .help = "Strong Tausworthe generator",
+                         },
+                         { .ival = "lfsr",
+                           .oval = FIO_RAND_GEN_LFSR,
+                           .help = "Variable length LFSR",
+                         },
+               },
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RANDOM,
+       },
+       {
+               .name   = "random_distribution",
+               .type   = FIO_OPT_STR,
+               .off1   = td_var_offset(random_distribution),
+               .cb     = str_random_distribution_cb,
+               .help   = "Random offset distribution generator",
+               .def    = "random",
+               .posval = {
+                         { .ival = "random",
+                           .oval = FIO_RAND_DIST_RANDOM,
+                           .help = "Completely random",
+                         },
+                         { .ival = "zipf",
+                           .oval = FIO_RAND_DIST_ZIPF,
+                           .help = "Zipf distribution",
+                         },
+                         { .ival = "pareto",
+                           .oval = FIO_RAND_DIST_PARETO,
+                           .help = "Pareto distribution",
+                         },
+               },
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RANDOM,
        },
        {
                .name   = "nrfiles",
+               .lname  = "Number of files",
                .alias  = "nr_files",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(nr_files),
                .help   = "Split job workload between this number of files",
                .def    = "1",
+               .interval = 1,
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "openfiles",
+               .lname  = "Number of open files",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(open_files),
                .help   = "Number of files to keep open at the same time",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "file_service_type",
+               .lname  = "File service type",
                .type   = FIO_OPT_STR,
                .cb     = str_fst_cb,
                .off1   = td_var_offset(file_service_type),
                .help   = "How to select which file to service next",
                .def    = "roundrobin",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
                .posval = {
                          { .ival = "random",
                            .oval = FIO_FSERVICE_RANDOM,
@@ -1246,14 +1703,18 @@ static struct fio_option options[FIO_MAX_OPTS] = {
                          },
                },
                .parent = "nrfiles",
+               .hide   = 1,
        },
 #ifdef FIO_HAVE_FALLOCATE
        {
                .name   = "fallocate",
+               .lname  = "Fallocate",
                .type   = FIO_OPT_STR,
                .off1   = td_var_offset(fallocate_mode),
                .help   = "Whether pre-allocation is performed when laying out files",
                .def    = "posix",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
                .posval = {
                          { .ival = "none",
                            .oval = FIO_FALLOCATE_NONE,
@@ -1283,35 +1744,51 @@ static struct fio_option options[FIO_MAX_OPTS] = {
 #endif /* FIO_HAVE_FALLOCATE */
        {
                .name   = "fadvise_hint",
+               .lname  = "Fadvise hint",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(fadvise_hint),
                .help   = "Use fadvise() to advise the kernel on IO pattern",
                .def    = "1",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "fsync",
+               .lname  = "Fsync",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(fsync_blocks),
                .help   = "Issue fsync for writes every given number of blocks",
                .def    = "0",
+               .interval = 1,
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "fdatasync",
+               .lname  = "Fdatasync",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(fdatasync_blocks),
                .help   = "Issue fdatasync for writes every given number of blocks",
                .def    = "0",
+               .interval = 1,
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "write_barrier",
+               .lname  = "Write barrier",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(barrier_blocks),
                .help   = "Make every Nth write a barrier write",
                .def    = "0",
+               .interval = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_INVALID,
        },
 #ifdef FIO_HAVE_SYNC_FILE_RANGE
        {
                .name   = "sync_file_range",
+               .lname  = "Sync file range",
                .posval = {
                          { .ival = "wait_before",
                            .oval = SYNC_FILE_RANGE_WAIT_BEFORE,
@@ -1334,77 +1811,113 @@ static struct fio_option options[FIO_MAX_OPTS] = {
                .cb     = str_sfr_cb,
                .off1   = td_var_offset(sync_file_range),
                .help   = "Use sync_file_range()",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
 #endif
        {
                .name   = "direct",
+               .lname  = "Direct I/O",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(odirect),
                .help   = "Use O_DIRECT IO (negates buffered)",
                .def    = "0",
+               .inverse = "buffered",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_TYPE,
        },
        {
                .name   = "buffered",
+               .lname  = "Buffered I/O",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(odirect),
                .neg    = 1,
                .help   = "Use buffered IO (negates direct)",
                .def    = "1",
+               .inverse = "direct",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_TYPE,
        },
        {
                .name   = "overwrite",
+               .lname  = "Overwrite",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(overwrite),
                .help   = "When writing, set whether to overwrite current data",
                .def    = "0",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "loops",
+               .lname  = "Loops",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(loops),
                .help   = "Number of times to run the job",
                .def    = "1",
+               .interval = 1,
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_RUNTIME,
        },
        {
                .name   = "numjobs",
+               .lname  = "Number of jobs",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(numjobs),
                .help   = "Duplicate this job this many times",
                .def    = "1",
+               .interval = 1,
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_RUNTIME,
        },
        {
                .name   = "startdelay",
+               .lname  = "Start delay",
                .type   = FIO_OPT_STR_VAL_TIME,
                .off1   = td_var_offset(start_delay),
                .help   = "Only start job when this period has passed",
                .def    = "0",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_RUNTIME,
        },
        {
                .name   = "runtime",
+               .lname  = "Runtime",
                .alias  = "timeout",
                .type   = FIO_OPT_STR_VAL_TIME,
                .off1   = td_var_offset(timeout),
                .help   = "Stop workload when this amount of time has passed",
                .def    = "0",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_RUNTIME,
        },
        {
                .name   = "time_based",
+               .lname  = "Time based",
                .type   = FIO_OPT_STR_SET,
                .off1   = td_var_offset(time_based),
                .help   = "Keep running until runtime/timeout is met",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_RUNTIME,
        },
        {
                .name   = "ramp_time",
+               .lname  = "Ramp time",
                .type   = FIO_OPT_STR_VAL_TIME,
                .off1   = td_var_offset(ramp_time),
                .help   = "Ramp up time before measuring performance",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_RUNTIME,
        },
        {
                .name   = "clocksource",
+               .lname  = "Clock source",
                .type   = FIO_OPT_STR,
                .cb     = fio_clock_source_cb,
                .off1   = td_var_offset(clocksource),
                .help   = "What type of timing source to use",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_CLOCK,
                .posval = {
                          { .ival = "gettimeofday",
                            .oval = CS_GTOD,
@@ -1425,11 +1938,14 @@ static struct fio_option options[FIO_MAX_OPTS] = {
        {
                .name   = "mem",
                .alias  = "iomem",
+               .lname  = "I/O Memory",
                .type   = FIO_OPT_STR,
                .cb     = str_mem_cb,
                .off1   = td_var_offset(mem_type),
                .help   = "Backing type for IO buffers",
                .def    = "malloc",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_INVALID,
                .posval = {
                          { .ival = "malloc",
                            .oval = MEM_MALLOC,
@@ -1460,20 +1976,26 @@ static struct fio_option options[FIO_MAX_OPTS] = {
        {
                .name   = "iomem_align",
                .alias  = "mem_align",
+               .lname  = "I/O memory alignment",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(mem_align),
                .minval = 0,
                .help   = "IO memory buffer offset alignment",
                .def    = "0",
                .parent = "iomem",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "verify",
+               .lname  = "Verify",
                .type   = FIO_OPT_STR,
                .off1   = td_var_offset(verify),
                .help   = "Verify data written",
-               .cb     = str_verify_cb,
                .def    = "0",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_VERIFY,
                .posval = {
                          { .ival = "0",
                            .oval = VERIFY_NONE,
@@ -1492,12 +2014,12 @@ static struct fio_option options[FIO_MAX_OPTS] = {
                            .help = "Use crc32 checksums for verification",
                          },
                          { .ival = "crc32c-intel",
-                           .oval = VERIFY_CRC32C_INTEL,
-                           .help = "Use hw crc32c checksums for verification",
+                           .oval = VERIFY_CRC32C,
+                           .help = "Use crc32c checksums for verification (hw assisted, if available)",
                          },
                          { .ival = "crc32c",
                            .oval = VERIFY_CRC32C,
-                           .help = "Use crc32c checksums for verification",
+                           .help = "Use crc32c checksums for verification (hw assisted, if available)",
                          },
                          { .ival = "crc16",
                            .oval = VERIFY_CRC16,
@@ -1532,495 +2054,787 @@ static struct fio_option options[FIO_MAX_OPTS] = {
        },
        {
                .name   = "do_verify",
+               .lname  = "Perform verify step",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(do_verify),
                .help   = "Run verification stage after write",
                .def    = "1",
                .parent = "verify",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_VERIFY,
        },
        {
                .name   = "verifysort",
+               .lname  = "Verify sort",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(verifysort),
                .help   = "Sort written verify blocks for read back",
                .def    = "1",
                .parent = "verify",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_VERIFY,
        },
        {
                .name   = "verify_interval",
+               .lname  = "Verify interval",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(verify_interval),
                .minval = 2 * sizeof(struct verify_header),
                .help   = "Store verify buffer header every N bytes",
                .parent = "verify",
+               .hide   = 1,
+               .interval = 2 * sizeof(struct verify_header),
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_VERIFY,
        },
        {
                .name   = "verify_offset",
+               .lname  = "Verify offset",
                .type   = FIO_OPT_INT,
                .help   = "Offset verify header location by N bytes",
-               .def    = "0",
-               .cb     = str_verify_offset_cb,
+               .off1   = td_var_offset(verify_offset),
+               .minval = sizeof(struct verify_header),
                .parent = "verify",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_VERIFY,
        },
        {
                .name   = "verify_pattern",
+               .lname  = "Verify pattern",
                .type   = FIO_OPT_STR,
                .cb     = str_verify_pattern_cb,
                .help   = "Fill pattern for IO buffers",
                .parent = "verify",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_VERIFY,
        },
        {
                .name   = "verify_fatal",
+               .lname  = "Verify fatal",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(verify_fatal),
                .def    = "0",
                .help   = "Exit on a single verify failure, don't continue",
                .parent = "verify",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_VERIFY,
        },
        {
                .name   = "verify_dump",
+               .lname  = "Verify dump",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(verify_dump),
-               .def    = "1",
+               .def    = "0",
                .help   = "Dump contents of good and bad blocks on failure",
                .parent = "verify",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_VERIFY,
        },
        {
                .name   = "verify_async",
+               .lname  = "Verify asynchronously",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(verify_async),
                .def    = "0",
                .help   = "Number of async verifier threads to use",
                .parent = "verify",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_VERIFY,
        },
        {
                .name   = "verify_backlog",
+               .lname  = "Verify backlog",
                .type   = FIO_OPT_STR_VAL,
                .off1   = td_var_offset(verify_backlog),
                .help   = "Verify after this number of blocks are written",
                .parent = "verify",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_VERIFY,
        },
        {
                .name   = "verify_backlog_batch",
+               .lname  = "Verify backlog batch",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(verify_batch),
                .help   = "Verify this number of IO blocks",
                .parent = "verify",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_VERIFY,
        },
 #ifdef FIO_HAVE_CPU_AFFINITY
        {
                .name   = "verify_async_cpus",
+               .lname  = "Async verify CPUs",
                .type   = FIO_OPT_STR,
                .cb     = str_verify_cpus_allowed_cb,
                .help   = "Set CPUs allowed for async verify threads",
                .parent = "verify_async",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_VERIFY,
        },
 #endif
 #ifdef FIO_HAVE_TRIM
        {
                .name   = "trim_percentage",
+               .lname  = "Trim percentage",
                .type   = FIO_OPT_INT,
-               .cb     = str_verify_trim_cb,
+               .off1   = td_var_offset(trim_percentage),
+               .minval = 0,
                .maxval = 100,
                .help   = "Number of verify blocks to discard/trim",
                .parent = "verify",
                .def    = "0",
+               .interval = 1,
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_TRIM,
        },
        {
                .name   = "trim_verify_zero",
-               .type   = FIO_OPT_INT,
+               .lname  = "Verify trim zero",
+               .type   = FIO_OPT_BOOL,
                .help   = "Verify that trim/discarded blocks are returned as zeroes",
                .off1   = td_var_offset(trim_zero),
                .parent = "trim_percentage",
+               .hide   = 1,
                .def    = "1",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_TRIM,
        },
        {
                .name   = "trim_backlog",
+               .lname  = "Trim backlog",
                .type   = FIO_OPT_STR_VAL,
                .off1   = td_var_offset(trim_backlog),
                .help   = "Trim after this number of blocks are written",
                .parent = "trim_percentage",
+               .hide   = 1,
+               .interval = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_TRIM,
        },
        {
                .name   = "trim_backlog_batch",
+               .lname  = "Trim backlog batch",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(trim_batch),
                .help   = "Trim this number of IO blocks",
                .parent = "trim_percentage",
+               .hide   = 1,
+               .interval = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_TRIM,
        },
 #endif
        {
                .name   = "write_iolog",
+               .lname  = "Write I/O log",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(write_iolog_file),
                .help   = "Store IO pattern to file",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IOLOG,
        },
        {
                .name   = "read_iolog",
+               .lname  = "Read I/O log",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(read_iolog_file),
                .help   = "Playback IO pattern from file",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IOLOG,
        },
        {
                .name   = "replay_no_stall",
-               .type   = FIO_OPT_INT,
+               .lname  = "Don't stall on replay",
+               .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(no_stall),
                .def    = "0",
                .parent = "read_iolog",
+               .hide   = 1,
                .help   = "Playback IO pattern file as fast as possible without stalls",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IOLOG,
        },
        {
                .name   = "replay_redirect",
+               .lname  = "Redirect device for replay",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(replay_redirect),
                .parent = "read_iolog",
+               .hide   = 1,
                .help   = "Replay all I/O onto this device, regardless of trace device",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IOLOG,
        },
        {
                .name   = "exec_prerun",
+               .lname  = "Pre-execute runnable",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(exec_prerun),
                .help   = "Execute this file prior to running job",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "exec_postrun",
+               .lname  = "Post-execute runnable",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(exec_postrun),
                .help   = "Execute this file after running job",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_INVALID,
        },
 #ifdef FIO_HAVE_IOSCHED_SWITCH
        {
                .name   = "ioscheduler",
+               .lname  = "I/O scheduler",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(ioscheduler),
                .help   = "Use this IO scheduler on the backing device",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
 #endif
        {
                .name   = "zonesize",
+               .lname  = "Zone size",
                .type   = FIO_OPT_STR_VAL,
                .off1   = td_var_offset(zone_size),
+               .help   = "Amount of data to read per zone",
+               .def    = "0",
+               .interval = 1024 * 1024,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_ZONE,
+       },
+       {
+               .name   = "zonerange",
+               .lname  = "Zone range",
+               .type   = FIO_OPT_STR_VAL,
+               .off1   = td_var_offset(zone_range),
                .help   = "Give size of an IO zone",
                .def    = "0",
+               .interval = 1024 * 1024,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_ZONE,
        },
        {
                .name   = "zoneskip",
+               .lname  = "Zone skip",
                .type   = FIO_OPT_STR_VAL,
                .off1   = td_var_offset(zone_skip),
                .help   = "Space between IO zones",
                .def    = "0",
+               .interval = 1024 * 1024,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_ZONE,
        },
        {
                .name   = "lockmem",
+               .lname  = "Lock memory",
                .type   = FIO_OPT_STR_VAL,
-               .cb     = str_lockmem_cb,
+               .off1   = td_var_offset(lockmem),
                .help   = "Lock down this amount of memory",
                .def    = "0",
+               .interval = 1024 * 1024,
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "rwmixread",
+               .lname  = "Read/write mix read",
                .type   = FIO_OPT_INT,
                .cb     = str_rwmix_read_cb,
                .maxval = 100,
                .help   = "Percentage of mixed workload that is reads",
                .def    = "50",
+               .interval = 5,
+               .inverse = "rwmixwrite",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RWMIX,
        },
        {
                .name   = "rwmixwrite",
+               .lname  = "Read/write mix write",
                .type   = FIO_OPT_INT,
                .cb     = str_rwmix_write_cb,
                .maxval = 100,
                .help   = "Percentage of mixed workload that is writes",
                .def    = "50",
+               .interval = 5,
+               .inverse = "rwmixread",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RWMIX,
        },
        {
                .name   = "rwmixcycle",
+               .lname  = "Read/write mix cycle",
                .type   = FIO_OPT_DEPRECATED,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RWMIX,
        },
        {
                .name   = "nice",
+               .lname  = "Nice",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(nice),
                .help   = "Set job CPU nice value",
                .minval = -19,
                .maxval = 20,
                .def    = "0",
+               .interval = 1,
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_CRED,
        },
 #ifdef FIO_HAVE_IOPRIO
        {
                .name   = "prio",
+               .lname  = "I/O nice priority",
                .type   = FIO_OPT_INT,
-               .cb     = str_prio_cb,
+               .off1   = td_var_offset(ioprio),
                .help   = "Set job IO priority value",
                .minval = 0,
                .maxval = 7,
+               .interval = 1,
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_CRED,
        },
        {
                .name   = "prioclass",
+               .lname  = "I/O nice priority class",
                .type   = FIO_OPT_INT,
-               .cb     = str_prioclass_cb,
+               .off1   = td_var_offset(ioprio_class),
                .help   = "Set job IO priority class",
                .minval = 0,
                .maxval = 3,
+               .interval = 1,
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_CRED,
        },
 #endif
        {
                .name   = "thinktime",
+               .lname  = "Thinktime",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(thinktime),
                .help   = "Idle time between IO buffers (usec)",
                .def    = "0",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_THINKTIME,
        },
        {
                .name   = "thinktime_spin",
+               .lname  = "Thinktime spin",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(thinktime_spin),
                .help   = "Start think time by spinning this amount (usec)",
                .def    = "0",
                .parent = "thinktime",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_THINKTIME,
        },
        {
                .name   = "thinktime_blocks",
+               .lname  = "Thinktime blocks",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(thinktime_blocks),
                .help   = "IO buffer period between 'thinktime'",
                .def    = "1",
                .parent = "thinktime",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_THINKTIME,
        },
        {
                .name   = "rate",
+               .lname  = "I/O rate",
                .type   = FIO_OPT_INT,
-               .off1   = td_var_offset(rate[0]),
-               .off2   = td_var_offset(rate[1]),
+               .off1   = td_var_offset(rate[DDIR_READ]),
+               .off2   = td_var_offset(rate[DDIR_WRITE]),
+               .off3   = td_var_offset(rate[DDIR_TRIM]),
                .help   = "Set bandwidth rate",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RATE,
        },
        {
                .name   = "ratemin",
+               .lname  = "I/O min rate",
                .type   = FIO_OPT_INT,
-               .off1   = td_var_offset(ratemin[0]),
-               .off2   = td_var_offset(ratemin[1]),
+               .off1   = td_var_offset(ratemin[DDIR_READ]),
+               .off2   = td_var_offset(ratemin[DDIR_WRITE]),
+               .off3   = td_var_offset(ratemin[DDIR_TRIM]),
                .help   = "Job must meet this rate or it will be shutdown",
                .parent = "rate",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RATE,
        },
        {
                .name   = "rate_iops",
+               .lname  = "I/O rate IOPS",
                .type   = FIO_OPT_INT,
-               .off1   = td_var_offset(rate_iops[0]),
-               .off2   = td_var_offset(rate_iops[1]),
+               .off1   = td_var_offset(rate_iops[DDIR_READ]),
+               .off2   = td_var_offset(rate_iops[DDIR_WRITE]),
+               .off3   = td_var_offset(rate_iops[DDIR_TRIM]),
                .help   = "Limit IO used to this number of IO operations/sec",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RATE,
        },
        {
                .name   = "rate_iops_min",
+               .lname  = "I/O min rate IOPS",
                .type   = FIO_OPT_INT,
-               .off1   = td_var_offset(rate_iops_min[0]),
-               .off2   = td_var_offset(rate_iops_min[1]),
+               .off1   = td_var_offset(rate_iops_min[DDIR_READ]),
+               .off2   = td_var_offset(rate_iops_min[DDIR_WRITE]),
+               .off3   = td_var_offset(rate_iops_min[DDIR_TRIM]),
                .help   = "Job must meet this rate or it will be shut down",
                .parent = "rate_iops",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RATE,
        },
        {
                .name   = "ratecycle",
+               .lname  = "I/O rate cycle",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(ratecycle),
                .help   = "Window average for rate limits (msec)",
                .def    = "1000",
                .parent = "rate",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_RATE,
+       },
+       {
+               .name   = "max_latency",
+               .type   = FIO_OPT_INT,
+               .off1   = td_var_offset(max_latency),
+               .help   = "Maximum tolerated IO latency (usec)",
+               .category = FIO_OPT_C_IO,
+               .group = FIO_OPT_G_RATE,
        },
        {
                .name   = "invalidate",
+               .lname  = "Cache invalidate",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(invalidate_cache),
                .help   = "Invalidate buffer/page cache prior to running job",
                .def    = "1",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_TYPE,
        },
        {
                .name   = "sync",
+               .lname  = "Synchronous I/O",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(sync_io),
                .help   = "Use O_SYNC for buffered writes",
                .def    = "0",
                .parent = "buffered",
-       },
-       {
-               .name   = "bwavgtime",
-               .type   = FIO_OPT_INT,
-               .off1   = td_var_offset(bw_avg_time),
-               .help   = "Time window over which to calculate bandwidth"
-                         " (msec)",
-               .def    = "500",
-               .parent = "write_bw_log",
-       },
-       {
-               .name   = "iopsavgtime",
-               .type   = FIO_OPT_INT,
-               .off1   = td_var_offset(iops_avg_time),
-               .help   = "Time window over which to calculate IOPS (msec)",
-               .def    = "500",
-               .parent = "write_iops_log",
+               .hide   = 1,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_TYPE,
        },
        {
                .name   = "create_serialize",
+               .lname  = "Create serialize",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(create_serialize),
                .help   = "Serialize creating of job files",
                .def    = "1",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "create_fsync",
+               .lname  = "Create fsync",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(create_fsync),
                .help   = "fsync file after creation",
                .def    = "1",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "create_on_open",
+               .lname  = "Create on open",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(create_on_open),
                .help   = "Create files when they are opened for IO",
                .def    = "0",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
+       },
+       {
+               .name   = "create_only",
+               .type   = FIO_OPT_BOOL,
+               .off1   = td_var_offset(create_only),
+               .help   = "Only perform file creation phase",
+               .category = FIO_OPT_C_FILE,
+               .def    = "0",
        },
        {
                .name   = "pre_read",
+               .lname  = "Pre-read files",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(pre_read),
                .help   = "Pre-read files before starting official testing",
                .def    = "0",
-       },
-       {
-               .name   = "cpuload",
-               .type   = FIO_OPT_INT,
-               .off1   = td_var_offset(cpuload),
-               .help   = "Use this percentage of CPU",
-       },
-       {
-               .name   = "cpuchunks",
-               .type   = FIO_OPT_INT,
-               .off1   = td_var_offset(cpucycle),
-               .help   = "Length of the CPU burn cycles (usecs)",
-               .def    = "50000",
-               .parent = "cpuload",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
 #ifdef FIO_HAVE_CPU_AFFINITY
        {
                .name   = "cpumask",
+               .lname  = "CPU mask",
                .type   = FIO_OPT_INT,
                .cb     = str_cpumask_cb,
                .help   = "CPU affinity mask",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_CRED,
        },
        {
                .name   = "cpus_allowed",
+               .lname  = "CPUs allowed",
                .type   = FIO_OPT_STR,
                .cb     = str_cpus_allowed_cb,
                .help   = "Set CPUs allowed",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_CRED,
+       },
+#endif
+#ifdef FIO_HAVE_LIBNUMA
+       {
+               .name   = "numa_cpu_nodes",
+               .type   = FIO_OPT_STR,
+               .cb     = str_numa_cpunodes_cb,
+               .help   = "NUMA CPU nodes bind",
+       },
+       {
+               .name   = "numa_mem_policy",
+               .type   = FIO_OPT_STR,
+               .cb     = str_numa_mpol_cb,
+               .help   = "NUMA memory policy setup",
        },
 #endif
        {
                .name   = "end_fsync",
+               .lname  = "End fsync",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(end_fsync),
                .help   = "Include fsync at the end of job",
                .def    = "0",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "fsync_on_close",
+               .lname  = "Fsync on close",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(fsync_on_close),
                .help   = "fsync files on close",
                .def    = "0",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "unlink",
+               .lname  = "Unlink file",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(unlink),
                .help   = "Unlink created files after job has completed",
                .def    = "0",
+               .category = FIO_OPT_C_FILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "exitall",
+               .lname  = "Exit-all on terminate",
                .type   = FIO_OPT_STR_SET,
                .cb     = str_exitall_cb,
                .help   = "Terminate all jobs when one exits",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_PROCESS,
        },
        {
                .name   = "stonewall",
+               .lname  = "Wait for previous",
                .alias  = "wait_for_previous",
                .type   = FIO_OPT_STR_SET,
                .off1   = td_var_offset(stonewall),
                .help   = "Insert a hard barrier between this job and previous",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_PROCESS,
        },
        {
                .name   = "new_group",
+               .lname  = "New group",
                .type   = FIO_OPT_STR_SET,
                .off1   = td_var_offset(new_group),
                .help   = "Mark the start of a new group (for reporting)",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_PROCESS,
        },
        {
                .name   = "thread",
+               .lname  = "Thread",
                .type   = FIO_OPT_STR_SET,
                .off1   = td_var_offset(use_thread),
-               .help   = "Use threads instead of forks",
+               .help   = "Use threads instead of processes",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_PROCESS,
        },
        {
                .name   = "write_bw_log",
-               .type   = FIO_OPT_STR,
-               .off1   = td_var_offset(write_bw_log),
-               .cb     = str_write_bw_log_cb,
+               .lname  = "Write bandwidth log",
+               .type   = FIO_OPT_STR_STORE,
+               .off1   = td_var_offset(bw_log_file),
                .help   = "Write log of bandwidth during run",
+               .category = FIO_OPT_C_LOG,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "write_lat_log",
-               .type   = FIO_OPT_STR,
-               .off1   = td_var_offset(write_lat_log),
-               .cb     = str_write_lat_log_cb,
+               .lname  = "Write latency log",
+               .type   = FIO_OPT_STR_STORE,
+               .off1   = td_var_offset(lat_log_file),
                .help   = "Write log of latency during run",
+               .category = FIO_OPT_C_LOG,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "write_iops_log",
+               .lname  = "Write IOPS log",
                .type   = FIO_OPT_STR,
-               .off1   = td_var_offset(write_iops_log),
-               .cb     = str_write_iops_log_cb,
+               .off1   = td_var_offset(iops_log_file),
                .help   = "Write log of IOPS during run",
+               .category = FIO_OPT_C_LOG,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
-               .name   = "hugepage-size",
+               .name   = "log_avg_msec",
+               .lname  = "Log averaging (msec)",
                .type   = FIO_OPT_INT,
-               .off1   = td_var_offset(hugepage_size),
-               .help   = "When using hugepages, specify size of each page",
-               .def    = __fio_stringify(FIO_HUGE_PAGE),
+               .off1   = td_var_offset(log_avg_msec),
+               .help   = "Average bw/iops/lat logs over this period of time",
+               .def    = "0",
+               .category = FIO_OPT_C_LOG,
+               .group  = FIO_OPT_G_INVALID,
+       },
+       {
+               .name   = "bwavgtime",
+               .lname  = "Bandwidth average time",
+               .type   = FIO_OPT_INT,
+               .off1   = td_var_offset(bw_avg_time),
+               .help   = "Time window over which to calculate bandwidth"
+                         " (msec)",
+               .def    = "500",
+               .parent = "write_bw_log",
+               .hide   = 1,
+               .interval = 100,
+               .category = FIO_OPT_C_LOG,
+               .group  = FIO_OPT_G_INVALID,
+       },
+       {
+               .name   = "iopsavgtime",
+               .lname  = "IOPS average time",
+               .type   = FIO_OPT_INT,
+               .off1   = td_var_offset(iops_avg_time),
+               .help   = "Time window over which to calculate IOPS (msec)",
+               .def    = "500",
+               .parent = "write_iops_log",
+               .hide   = 1,
+               .interval = 100,
+               .category = FIO_OPT_C_LOG,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "group_reporting",
-               .type   = FIO_OPT_STR_SET,
+               .lname  = "Group reporting",
+               .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(group_reporting),
                .help   = "Do reporting on a per-group basis",
+               .def    = "1",
+               .category = FIO_OPT_C_STAT,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "zero_buffers",
+               .lname  = "Zero I/O buffers",
                .type   = FIO_OPT_STR_SET,
                .off1   = td_var_offset(zero_buffers),
                .help   = "Init IO buffers to all zeroes",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_BUF,
        },
        {
                .name   = "refill_buffers",
+               .lname  = "Refill I/O buffers",
                .type   = FIO_OPT_STR_SET,
                .off1   = td_var_offset(refill_buffers),
                .help   = "Refill IO buffers on every IO submit",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_BUF,
        },
        {
                .name   = "scramble_buffers",
+               .lname  = "Scramble I/O buffers",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(scramble_buffers),
                .help   = "Slightly scramble buffers on every IO submit",
                .def    = "1",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_BUF,
+       },
+       {
+               .name   = "buffer_compress_percentage",
+               .lname  = "Buffer compression percentage",
+               .type   = FIO_OPT_INT,
+               .off1   = td_var_offset(compress_percentage),
+               .maxval = 100,
+               .minval = 1,
+               .help   = "How compressible the buffer is (approximately)",
+               .interval = 5,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_BUF,
+       },
+       {
+               .name   = "buffer_compress_chunk",
+               .lname  = "Buffer compression chunk size",
+               .type   = FIO_OPT_INT,
+               .off1   = td_var_offset(compress_chunk),
+               .parent = "buffer_compress_percentage",
+               .hide   = 1,
+               .help   = "Size of compressible region in buffer",
+               .interval = 256,
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_BUF,
        },
        {
                .name   = "clat_percentiles",
+               .lname  = "Completion latency percentiles",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(clat_percentiles),
                .help   = "Enable the reporting of completion latency percentiles",
                .def    = "1",
+               .category = FIO_OPT_C_STAT,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "percentile_list",
+               .lname  = "Completion latency percentile list",
                .type   = FIO_OPT_FLOAT_LIST,
                .off1   = td_var_offset(percentile_list),
                .off2   = td_var_offset(overwrite_plist),
@@ -2028,108 +2842,283 @@ static struct fio_option options[FIO_MAX_OPTS] = {
                .maxlen = FIO_IO_U_LIST_MAX_LEN,
                .minfp  = 0.0,
                .maxfp  = 100.0,
+               .category = FIO_OPT_C_STAT,
+               .group  = FIO_OPT_G_INVALID,
        },
 
 #ifdef FIO_HAVE_DISK_UTIL
        {
                .name   = "disk_util",
+               .lname  = "Disk utilization",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(do_disk_util),
                .help   = "Log disk utilization statistics",
                .def    = "1",
+               .category = FIO_OPT_C_STAT,
+               .group  = FIO_OPT_G_INVALID,
        },
 #endif
        {
                .name   = "gtod_reduce",
+               .lname  = "Reduce gettimeofday() calls",
                .type   = FIO_OPT_BOOL,
                .help   = "Greatly reduce number of gettimeofday() calls",
                .cb     = str_gtod_reduce_cb,
                .def    = "0",
+               .hide_on_set = 1,
+               .category = FIO_OPT_C_STAT,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "disable_lat",
+               .lname  = "Disable all latency stats",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(disable_lat),
                .help   = "Disable latency numbers",
                .parent = "gtod_reduce",
+               .hide   = 1,
                .def    = "0",
+               .category = FIO_OPT_C_STAT,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "disable_clat",
+               .lname  = "Disable completion latency stats",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(disable_clat),
                .help   = "Disable completion latency numbers",
                .parent = "gtod_reduce",
+               .hide   = 1,
                .def    = "0",
+               .category = FIO_OPT_C_STAT,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "disable_slat",
+               .lname  = "Disable submission latency stats",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(disable_slat),
                .help   = "Disable submission latency numbers",
                .parent = "gtod_reduce",
+               .hide   = 1,
                .def    = "0",
+               .category = FIO_OPT_C_STAT,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "disable_bw_measurement",
+               .lname  = "Disable bandwidth stats",
                .type   = FIO_OPT_BOOL,
                .off1   = td_var_offset(disable_bw),
                .help   = "Disable bandwidth logging",
                .parent = "gtod_reduce",
+               .hide   = 1,
                .def    = "0",
+               .category = FIO_OPT_C_STAT,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "gtod_cpu",
+               .lname  = "Dedicated gettimeofday() CPU",
                .type   = FIO_OPT_INT,
                .cb     = str_gtod_cpu_cb,
                .help   = "Set up dedicated gettimeofday() thread on this CPU",
                .verify = gtod_cpu_verify,
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_CLOCK,
        },
        {
                .name   = "continue_on_error",
-               .type   = FIO_OPT_BOOL,
+               .lname  = "Continue on error",
+               .type   = FIO_OPT_STR,
                .off1   = td_var_offset(continue_on_error),
                .help   = "Continue on non-fatal errors during IO",
+               .def    = "none",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_ERR,
+               .posval = {
+                         { .ival = "none",
+                           .oval = ERROR_TYPE_NONE,
+                           .help = "Exit when an error is encountered",
+                         },
+                         { .ival = "read",
+                           .oval = ERROR_TYPE_READ,
+                           .help = "Continue on read errors only",
+                         },
+                         { .ival = "write",
+                           .oval = ERROR_TYPE_WRITE,
+                           .help = "Continue on write errors only",
+                         },
+                         { .ival = "io",
+                           .oval = ERROR_TYPE_READ | ERROR_TYPE_WRITE,
+                           .help = "Continue on any IO errors",
+                         },
+                         { .ival = "verify",
+                           .oval = ERROR_TYPE_VERIFY,
+                           .help = "Continue on verify errors only",
+                         },
+                         { .ival = "all",
+                           .oval = ERROR_TYPE_ANY,
+                           .help = "Continue on all io and verify errors",
+                         },
+                         { .ival = "0",
+                           .oval = ERROR_TYPE_NONE,
+                           .help = "Alias for 'none'",
+                         },
+                         { .ival = "1",
+                           .oval = ERROR_TYPE_ANY,
+                           .help = "Alias for 'all'",
+                         },
+               },
+       },
+       {
+               .name   = "ignore_error",
+               .type   = FIO_OPT_STR,
+               .cb     = str_ignore_error_cb,
+               .help   = "Set a specific list of errors to ignore",
+               .parent = "rw",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_ERR,
+       },
+       {
+               .name   = "error_dump",
+               .type   = FIO_OPT_BOOL,
+               .off1   = td_var_offset(error_dump),
                .def    = "0",
+               .help   = "Dump info on each error",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_ERR,
        },
        {
                .name   = "profile",
+               .lname  = "Profile",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(profile),
                .help   = "Select a specific builtin performance test",
+               .category = FIO_OPT_C_PROFILE,
+               .group  = FIO_OPT_G_INVALID,
        },
        {
                .name   = "cgroup",
+               .lname  = "Cgroup",
                .type   = FIO_OPT_STR_STORE,
                .off1   = td_var_offset(cgroup),
                .help   = "Add job to cgroup of this name",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_CGROUP,
+       },
+       {
+               .name   = "cgroup_nodelete",
+               .lname  = "Cgroup no-delete",
+               .type   = FIO_OPT_BOOL,
+               .off1   = td_var_offset(cgroup_nodelete),
+               .help   = "Do not delete cgroups after job completion",
+               .def    = "0",
+               .parent = "cgroup",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_CGROUP,
        },
        {
                .name   = "cgroup_weight",
+               .lname  = "Cgroup weight",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(cgroup_weight),
                .help   = "Use given weight for cgroup",
                .minval = 100,
                .maxval = 1000,
-       },
-       {
-               .name   = "cgroup_nodelete",
-               .type   = FIO_OPT_BOOL,
-               .off1   = td_var_offset(cgroup_nodelete),
-               .help   = "Do not delete cgroups after job completion",
-               .def    = "0",
+               .parent = "cgroup",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_CGROUP,
        },
        {
                .name   = "uid",
+               .lname  = "User ID",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(uid),
                .help   = "Run job with this user ID",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_CRED,
        },
        {
                .name   = "gid",
+               .lname  = "Group ID",
                .type   = FIO_OPT_INT,
                .off1   = td_var_offset(gid),
                .help   = "Run job with this group ID",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_CRED,
+       },
+       {
+               .name   = "kb_base",
+               .lname  = "KB Base",
+               .type   = FIO_OPT_INT,
+               .off1   = td_var_offset(kb_base),
+               .verify = kb_base_verify,
+               .prio   = 1,
+               .def    = "1024",
+               .help   = "How many bytes per KB for reporting (1000 or 1024)",
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_INVALID,
+       },
+       {
+               .name   = "hugepage-size",
+               .lname  = "Hugepage size",
+               .type   = FIO_OPT_INT,
+               .off1   = td_var_offset(hugepage_size),
+               .help   = "When using hugepages, specify size of each page",
+               .def    = __fio_stringify(FIO_HUGE_PAGE),
+               .interval = 1024 * 1024,
+               .category = FIO_OPT_C_GENERAL,
+               .group  = FIO_OPT_G_INVALID,
+       },
+       {
+               .name   = "flow_id",
+               .lname  = "I/O flow ID",
+               .type   = FIO_OPT_INT,
+               .off1   = td_var_offset(flow_id),
+               .help   = "The flow index ID to use",
+               .def    = "0",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_FLOW,
+       },
+       {
+               .name   = "flow",
+               .lname  = "I/O flow weight",
+               .type   = FIO_OPT_INT,
+               .off1   = td_var_offset(flow),
+               .help   = "Weight for flow control of this job",
+               .parent = "flow_id",
+               .hide   = 1,
+               .def    = "0",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_FLOW,
+       },
+       {
+               .name   = "flow_watermark",
+               .lname  = "I/O flow watermark",
+               .type   = FIO_OPT_INT,
+               .off1   = td_var_offset(flow_watermark),
+               .help   = "High watermark for flow control. This option"
+                       " should be set to the same value for all threads"
+                       " with non-zero flow.",
+               .parent = "flow_id",
+               .hide   = 1,
+               .def    = "1024",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_FLOW,
+       },
+       {
+               .name   = "flow_sleep",
+               .lname  = "I/O flow sleep",
+               .type   = FIO_OPT_INT,
+               .off1   = td_var_offset(flow_sleep),
+               .help   = "How many microseconds to sleep after being held"
+                       " back by the flow control mechanism",
+               .parent = "flow_id",
+               .hide   = 1,
+               .def    = "0",
+               .category = FIO_OPT_C_IO,
+               .group  = FIO_OPT_G_IO_FLOW,
        },
        {
                .name = NULL,
@@ -2137,33 +3126,26 @@ static struct fio_option options[FIO_MAX_OPTS] = {
 };
 
 static void add_to_lopt(struct option *lopt, struct fio_option *o,
-                       const char *name)
+                       const char *name, int val)
 {
        lopt->name = (char *) name;
-       lopt->val = FIO_GETOPT_JOB;
+       lopt->val = val;
        if (o->type == FIO_OPT_STR_SET)
                lopt->has_arg = no_argument;
        else
                lopt->has_arg = required_argument;
 }
 
-void fio_options_dup_and_init(struct option *long_options)
+static void options_to_lopts(struct fio_option *opts,
+                             struct option *long_options,
+                             int i, int option_type)
 {
-       struct fio_option *o;
-       unsigned int i;
-
-       options_init(options);
-
-       i = 0;
-       while (long_options[i].name)
-               i++;
-
-       o = &options[0];
+       struct fio_option *o = &opts[0];
        while (o->name) {
-               add_to_lopt(&long_options[i], o, o->name);
+               add_to_lopt(&long_options[i], o, o->name, option_type);
                if (o->alias) {
                        i++;
-                       add_to_lopt(&long_options[i], o, o->alias);
+                       add_to_lopt(&long_options[i], o, o->alias, option_type);
                }
 
                i++;
@@ -2172,6 +3154,43 @@ void fio_options_dup_and_init(struct option *long_options)
        }
 }
 
+void fio_options_set_ioengine_opts(struct option *long_options,
+                                  struct thread_data *td)
+{
+       unsigned int i;
+
+       i = 0;
+       while (long_options[i].name) {
+               if (long_options[i].val == FIO_GETOPT_IOENGINE) {
+                       memset(&long_options[i], 0, sizeof(*long_options));
+                       break;
+               }
+               i++;
+       }
+
+       /*
+        * Just clear out the prior ioengine options.
+        */
+       if (!td || !td->eo)
+               return;
+
+       options_to_lopts(td->io_ops->options, long_options, i,
+                        FIO_GETOPT_IOENGINE);
+}
+
+void fio_options_dup_and_init(struct option *long_options)
+{
+       unsigned int i;
+
+       options_init(fio_options);
+
+       i = 0;
+       while (long_options[i].name)
+               i++;
+
+       options_to_lopts(fio_options, long_options, i, FIO_GETOPT_JOB);
+}
+
 struct fio_keyword {
        const char *word;
        const char *desc;
@@ -2202,7 +3221,7 @@ void fio_keywords_init(void)
        char buf[128];
        long l;
 
-       sprintf(buf, "%lu", page_size);
+       sprintf(buf, "%lu", (unsigned long) page_size);
        fio_keywords[0].replace = strdup(buf);
 
        mb_memory = os_phys_mem() / (1024 * 1024);
@@ -2218,15 +3237,15 @@ void fio_keywords_init(void)
 
 static char *bc_calc(char *str)
 {
-       char *buf, *tmp, opt[80];
+       char buf[128], *tmp;
        FILE *f;
        int ret;
 
        /*
         * No math, just return string
         */
-       if (!strchr(str, '+') && !strchr(str, '-') && !strchr(str, '*') &&
-           !strchr(str, '/'))
+       if ((!strchr(str, '+') && !strchr(str, '-') && !strchr(str, '*') &&
+            !strchr(str, '/')) || strchr(str, '\''))
                return str;
 
        /*
@@ -2237,37 +3256,87 @@ static char *bc_calc(char *str)
                return str;
 
        tmp++;
-       memset(opt, 0, sizeof(opt));
-       strncpy(opt, str, tmp - str);
 
-       buf = malloc(128);
+       /*
+        * Prevent buffer overflows; such a case isn't reasonable anyway
+        */
+       if (strlen(str) >= 128 || strlen(tmp) > 100)
+               return str;
 
        sprintf(buf, "which %s > /dev/null", BC_APP);
        if (system(buf)) {
                log_err("fio: bc is needed for performing math\n");
-               free(buf);
                return NULL;
        }
 
-       sprintf(buf, "echo %s | %s", tmp, BC_APP);
+       sprintf(buf, "echo '%s' | %s", tmp, BC_APP);
        f = popen(buf, "r");
-       if (!f) {
-               free(buf);
+       if (!f)
                return NULL;
-       }
 
-       ret = fread(buf, 1, 128, f);
-       if (ret <= 0) {
-               free(buf);
+       ret = fread(&buf[tmp - str], 1, 128 - (tmp - str), f);
+       if (ret <= 0)
                return NULL;
-       }
 
-       buf[ret - 1] = '\0';
-       strcat(opt, buf);
-       strcpy(buf, opt);
        pclose(f);
+       buf[(tmp - str) + ret - 1] = '\0';
+       memcpy(buf, str, tmp - str);
        free(str);
-       return buf;
+       return strdup(buf);
+}
+
+/*
+ * Return a copy of the input string with substrings of the form ${VARNAME}
+ * substituted with the value of the environment variable VARNAME.  The
+ * substitution always occurs, even if VARNAME is empty or the corresponding
+ * environment variable undefined.
+ */
+static char *option_dup_subs(const char *opt)
+{
+       char out[OPT_LEN_MAX+1];
+       char in[OPT_LEN_MAX+1];
+       char *outptr = out;
+       char *inptr = in;
+       char *ch1, *ch2, *env;
+       ssize_t nchr = OPT_LEN_MAX;
+       size_t envlen;
+
+       if (strlen(opt) + 1 > OPT_LEN_MAX) {
+               log_err("OPT_LEN_MAX (%d) is too small\n", OPT_LEN_MAX);
+               return NULL;
+       }
+
+       in[OPT_LEN_MAX] = '\0';
+       strncpy(in, opt, OPT_LEN_MAX);
+
+       while (*inptr && nchr > 0) {
+               if (inptr[0] == '$' && inptr[1] == '{') {
+                       ch2 = strchr(inptr, '}');
+                       if (ch2 && inptr+1 < ch2) {
+                               ch1 = inptr+2;
+                               inptr = ch2+1;
+                               *ch2 = '\0';
+
+                               env = getenv(ch1);
+                               if (env) {
+                                       envlen = strlen(env);
+                                       if (envlen <= nchr) {
+                                               memcpy(outptr, env, envlen);
+                                               outptr += envlen;
+                                               nchr -= envlen;
+                                       }
+                               }
+
+                               continue;
+                       }
+               }
+
+               *outptr++ = *inptr++;
+               --nchr;
+       }
+
+       *outptr = '\0';
+       return strdup(out);
 }
 
 /*
@@ -2277,6 +3346,7 @@ static char *fio_keyword_replace(char *opt)
 {
        char *s;
        int i;
+       int docalc = 0;
 
        for (i = 0; fio_keywords[i].word != NULL; i++) {
                struct fio_keyword *kw = &fio_keywords[i];
@@ -2306,76 +3376,148 @@ static char *fio_keyword_replace(char *opt)
                         * replace opt and free the old opt
                         */
                        opt = new;
-                       //free(o_org);
+                       free(o_org);
 
-                       /*
-                        * Check for potential math and invoke bc, if possible
-                        */
-                       opt = bc_calc(opt);
+                       docalc = 1;
                }
        }
 
+       /*
+        * Check for potential math and invoke bc, if possible
+        */
+       if (docalc)
+               opt = bc_calc(opt);
+
        return opt;
 }
 
+static char **dup_and_sub_options(char **opts, int num_opts)
+{
+       int i;
+       char **opts_copy = malloc(num_opts * sizeof(*opts));
+       for (i = 0; i < num_opts; i++) {
+               opts_copy[i] = option_dup_subs(opts[i]);
+               if (!opts_copy[i])
+                       continue;
+               opts_copy[i] = fio_keyword_replace(opts_copy[i]);
+       }
+       return opts_copy;
+}
+
 int fio_options_parse(struct thread_data *td, char **opts, int num_opts)
 {
-       int i, ret;
+       int i, ret, unknown;
+       char **opts_copy;
 
-       sort_options(opts, options, num_opts);
+       sort_options(opts, fio_options, num_opts);
+       opts_copy = dup_and_sub_options(opts, num_opts);
 
-       for (ret = 0, i = 0; i < num_opts; i++) {
-               opts[i] = fio_keyword_replace(opts[i]);
-               ret |= parse_option(opts[i], options, td);
+       for (ret = 0, i = 0, unknown = 0; i < num_opts; i++) {
+               struct fio_option *o;
+               int newret = parse_option(opts_copy[i], opts[i], fio_options,
+                                               &o, td);
+
+               if (opts_copy[i]) {
+                       if (newret && !o) {
+                               unknown++;
+                               continue;
+                       }
+                       free(opts_copy[i]);
+                       opts_copy[i] = NULL;
+               }
+
+               ret |= newret;
+       }
+
+       if (unknown) {
+               ret |= ioengine_load(td);
+               if (td->eo) {
+                       sort_options(opts_copy, td->io_ops->options, num_opts);
+                       opts = opts_copy;
+               }
+               for (i = 0; i < num_opts; i++) {
+                       struct fio_option *o = NULL;
+                       int newret = 1;
+                       if (!opts_copy[i])
+                               continue;
+
+                       if (td->eo)
+                               newret = parse_option(opts_copy[i], opts[i],
+                                                     td->io_ops->options, &o,
+                                                     td->eo);
+
+                       ret |= newret;
+                       if (!o)
+                               log_err("Bad option <%s>\n", opts[i]);
+
+                       free(opts_copy[i]);
+                       opts_copy[i] = NULL;
+               }
        }
 
+       free(opts_copy);
        return ret;
 }
 
 int fio_cmd_option_parse(struct thread_data *td, const char *opt, char *val)
 {
-       return parse_cmd_option(opt, val, options, td);
+       return parse_cmd_option(opt, val, fio_options, td);
+}
+
+int fio_cmd_ioengine_option_parse(struct thread_data *td, const char *opt,
+                               char *val)
+{
+       return parse_cmd_option(opt, val, td->io_ops->options, td);
 }
 
 void fio_fill_default_options(struct thread_data *td)
 {
-       fill_default_options(td, options);
+       fill_default_options(td, fio_options);
 }
 
 int fio_show_option_help(const char *opt)
 {
-       return show_cmd_help(options, opt);
+       return show_cmd_help(fio_options, opt);
 }
 
-/*
- * dupe FIO_OPT_STR_STORE options
- */
-void options_mem_dupe(struct thread_data *td)
+void options_mem_dupe(void *data, struct fio_option *options)
 {
-       struct thread_options *o = &td->o;
-       struct fio_option *opt;
+       struct fio_option *o;
        char **ptr;
-       int i;
 
-       for (i = 0, opt = &options[0]; opt->name; i++, opt = &options[i]) {
-               if (opt->type != FIO_OPT_STR_STORE)
+       for (o = &options[0]; o->name; o++) {
+               if (o->type != FIO_OPT_STR_STORE)
                        continue;
 
-               ptr = (void *) o + opt->off1;
-               if (!*ptr)
-                       ptr = td_var(o, opt->off1);
+               ptr = td_var(data, o->off1);
                if (*ptr)
                        *ptr = strdup(*ptr);
        }
 }
 
+/*
+ * dupe FIO_OPT_STR_STORE options
+ */
+void fio_options_mem_dupe(struct thread_data *td)
+{
+       options_mem_dupe(&td->o, fio_options);
+
+       if (td->eo && td->io_ops) {
+               void *oldeo = td->eo;
+
+               td->eo = malloc(td->io_ops->option_struct_size);
+               memcpy(td->eo, oldeo, td->io_ops->option_struct_size);
+               options_mem_dupe(td->eo, td->io_ops->options);
+       }
+}
+
 unsigned int fio_get_kb_base(void *data)
 {
-       struct thread_data *td = data;
+       struct thread_options *o = data;
        unsigned int kb_base = 0;
 
-       if (td)
-               kb_base = td->o.kb_base;
+       if (o)
+               kb_base = o->kb_base;
        if (!kb_base)
                kb_base = 1024;
 
@@ -2387,13 +3529,13 @@ int add_option(struct fio_option *o)
        struct fio_option *__o;
        int opt_index = 0;
 
-       __o = options;
+       __o = fio_options;
        while (__o->name) {
                opt_index++;
                __o++;
        }
 
-       memcpy(&options[opt_index], o, sizeof(*o));
+       memcpy(&fio_options[opt_index], o, sizeof(*o));
        return 0;
 }
 
@@ -2401,7 +3543,7 @@ void invalidate_profile_options(const char *prof_name)
 {
        struct fio_option *o;
 
-       o = options;
+       o = fio_options;
        while (o->name) {
                if (o->prof_name && !strcmp(o->prof_name, prof_name)) {
                        o->type = FIO_OPT_INVALID;
@@ -2416,7 +3558,7 @@ void add_opt_posval(const char *optname, const char *ival, const char *help)
        struct fio_option *o;
        unsigned int i;
 
-       o = find_option(options, optname);
+       o = find_option(fio_options, optname);
        if (!o)
                return;
 
@@ -2435,7 +3577,7 @@ void del_opt_posval(const char *optname, const char *ival)
        struct fio_option *o;
        unsigned int i;
 
-       o = find_option(options, optname);
+       o = find_option(fio_options, optname);
        if (!o)
                return;
 
@@ -2452,5 +3594,16 @@ void del_opt_posval(const char *optname, const char *ival)
 
 void fio_options_free(struct thread_data *td)
 {
-       options_free(options, td);
+       options_free(fio_options, td);
+       if (td->eo && td->io_ops && td->io_ops->options) {
+               options_free(td->io_ops->options, td->eo);
+               free(td->eo);
+               td->eo = NULL;
+       }
+}
+
+struct fio_option *fio_option_find(const char *name)
+{
+       return find_option(fio_options, name);
 }
+