clean up argparse usage
[fio.git] / tools / hist / fio-histo-log-pctiles.py
index 9fb846f14eacde93fc59afacc6cc0ce7d96929c6..c398113c12bd5423859bd3e01710fb5bd04f5570 100755 (executable)
@@ -23,7 +23,7 @@
 
 import sys, os, math, copy
 from copy import deepcopy
-
+import argparse
 import unittest2
 
 msec_per_sec = 1000
@@ -32,21 +32,12 @@ nsec_per_usec = 1000
 class FioHistoLogExc(Exception):
     pass
 
-# if there is an error, print message, print syntax, and exit with error status
+# if there is an error, print message, and exit with error status
 
-def usage(msg):
+def myabort(msg):
     print('ERROR: ' + msg)
-    print('usage: fio-histo-log-pctiles.py ')
-    print('  [ --fio-version 2|3 (default 3) ]')
-    print('  [ --bucket-groups positive-int (default 29) ]')
-    print('  [ --bucket-bits small-positive-int (default 6) ]')
-    print('  [ --percentiles p1,p2,...,pN ] (default 0,50,95,99,100)')
-    print('  [ --time-quantum positive-int (default 1 sec) ]')
-    print('  [ --output-unit msec|usec|nsec (default msec) ]')
-    print('  log-file1 log-file2 ...')
     sys.exit(1)
 
-
 # convert histogram log file into a list of
 # (time_ms, direction, bsz, buckets) tuples where
 # - time_ms is the time in msec at which the log record was written
@@ -269,19 +260,34 @@ def get_pctiles(buckets, wanted, time_ranges):
     # this prevents floating-point error from preventing loop exit
     almost_100 = 99.9999
 
+    # pct is the percentile corresponding to 
+    # all I/O requests up through bucket b
+    pct = 0.0
     total_so_far = 0
     for b, io_count in enumerate(buckets):
+        if io_count == 0:
+            continue
         total_so_far += io_count
-        pct_lt = 100.0 * float(total_so_far) / total_ios
+        # last_pct_lt is the percentile corresponding to 
+        # all I/O requests up to, but not including, bucket b
+        last_pct = pct
+        pct = 100.0 * float(total_so_far) / total_ios
         # a single bucket could satisfy multiple pctiles
         # so this must be a while loop
-        # consider both the 0-percentile (min latency)
-        # and 100-percentile (max latency) case here
-        while ((next_pctile == 100.0 and pct_lt >= almost_100) or
-               (next_pctile < 100.0  and pct_lt > next_pctile)):
-            # FIXME: interpolate between these fractions
+        # for 100-percentile (max latency) case, no bucket exceeds it 
+        # so we must stop there.
+        while ((next_pctile == 100.0 and pct >= almost_100) or
+               (next_pctile < 100.0  and pct > next_pctile)):
+            # interpolate between min and max time for bucket time interval
+            # we keep the time_ranges access inside this loop, 
+            # even though it could be above the loop,
+            # because in many cases we will not be even entering 
+            # the loop so we optimize out these accesses
             range_max_time = time_ranges[b][1]
-            pctile_result[next_pctile] = range_max_time
+            range_min_time = time_ranges[b][0]
+            offset_frac = (next_pctile - last_pct)/(pct - last_pct)
+            interpolation = range_min_time + (offset_frac*(range_max_time - range_min_time))
+            pctile_result[next_pctile] = interpolation
             pctile_index += 1
             if pctile_index == pctile_count:
                 break
@@ -292,99 +298,58 @@ def get_pctiles(buckets, wanted, time_ranges):
     return pctile_result
 
 
-# parse parameters 
-# returns a tuple of command line parameters
-# parameters have default values unless otherwise shown
-
-def parse_cli_params():
-    
-    # default values for input parameters
-
-    fio_version = 3        # we are using fio 3.x now
-    bucket_groups = None   # defaulting comes later
-    bucket_bits = 6        # default in fio 3.x
-    pctiles_wanted = [ 0, 50, 90, 95, 99, 100 ]
-    time_quantum = 1
-    output_unit = 'usec'
-
-    # parse command line parameters and display them
-    
-    argindex = 1
-    argct = len(sys.argv)
-    if argct < 2:
-        usage('must supply at least one histogram log file')
-    while argindex < argct:
-        if argct < argindex + 2:
-            break
-        pname = sys.argv[argindex]
-        pval = sys.argv[argindex+1]
-        if not pname.startswith('--'):
-            break
-        argindex += 2
-        pname = pname[2:]
-    
-        if pname == 'bucket-groups':
-            bucket_groups = int(pval)
-        elif pname == 'bucket-bits':
-            bucket_bits = int(pval)
-        elif pname == 'time-quantum':
-            time_quantum = int(pval)
-        elif pname == 'percentiles':
-            pctiles_wanted = [ float(p) for p in pval.split(',') ]
-        elif pname == 'output-unit':
-            if pval == 'msec' or pval == 'usec':
-                output_unit = pval
-            else:
-                usage('output-unit must be usec (microseconds) or msec (milliseconds)')
-        elif pname == 'fio-version':
-            if pval != '2' and pval != '3':
-                usage('invalid fio version, must be 2 or 3')
-            fio_version = int(pval)
-        else:
-            usage('invalid parameter name --%s' % pname)
-
-    if not bucket_groups:
-        # default changes based on fio version
-        if fio_version == 2:
-            bucket_groups = 19
-        else:
-            # default in fio 3.x
-            bucket_groups = 29
-
-    filename_list = sys.argv[argindex:]
-    for f in filename_list:
-        if not os.path.exists(f):
-            usage('file %s does not exist' % f)
-    return (bucket_groups, bucket_bits, fio_version, pctiles_wanted, 
-            filename_list, time_quantum, output_unit)
-
-
 # this is really the main program
 
 def compute_percentiles_from_logs():
-    (bucket_groups, bucket_bits, fio_version, pctiles_wanted, 
-     file_list, time_quantum, output_unit) = parse_cli_params()
-
-    print('bucket groups = %d' % bucket_groups)
-    print('bucket bits = %d' % bucket_bits)
-    print('time quantum = %d sec' % time_quantum)
-    print('percentiles = %s' % ','.join([ str(p) for p in pctiles_wanted ]))
-    buckets_per_group = 1 << bucket_bits
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--fio-version", dest="fio_version", 
+        default="3", choices=[2,3], type=int, 
+        help="fio version (default=3)")
+    parser.add_argument("--bucket-groups", dest="bucket_groups", default="29", type=int, 
+        help="fio histogram bucket groups (default=29)")
+    parser.add_argument("--bucket-bits", dest="bucket_bits", 
+        default="6", type=int, 
+        help="fio histogram buckets-per-group bits (default=6 means 64 buckets/group)")
+    parser.add_argument("--percentiles", dest="pctiles_wanted", 
+        default=[ 0., 50., 95., 99., 100.], type=float, nargs='+',
+        help="fio histogram buckets-per-group bits (default=6 means 64 buckets/group)")
+    parser.add_argument("--time-quantum", dest="time_quantum", 
+        default="1", type=int,
+        help="time quantum in seconds (default=1)")
+    parser.add_argument("--output-unit", dest="output_unit", 
+        default="usec", type=str,
+        help="Latency percentile output unit: msec|usec|nsec (default usec)")
+    parser.add_argument("file_list", nargs='+', 
+        help='list of files, preceded by " -- " if necessary')
+    args = parser.parse_args()
+
+    # default changes based on fio version
+    if args.fio_version == 2:
+        args.bucket_groups = 19
+
+    # print parameters
+
+    print('fio version = %d' % args.fio_version)
+    print('bucket groups = %d' % args.bucket_groups)
+    print('bucket bits = %d' % args.bucket_bits)
+    print('time quantum = %d sec' % args.time_quantum)
+    print('percentiles = %s' % ','.join([ str(p) for p in args.pctiles_wanted ]))
+    buckets_per_group = 1 << args.bucket_bits
     print('buckets per group = %d' % buckets_per_group)
-    buckets_per_interval = buckets_per_group * bucket_groups
+    buckets_per_interval = buckets_per_group * args.bucket_groups
     print('buckets per interval = %d ' % buckets_per_interval)
     bucket_index_range = range(0, buckets_per_interval)
-    if time_quantum == 0:
-        usage('time-quantum must be a positive number of seconds')
-    print('output unit = ' + output_unit)
-    if output_unit == 'msec':
+    if args.time_quantum == 0:
+        print('ERROR: time-quantum must be a positive number of seconds')
+    print('output unit = ' + args.output_unit)
+    if args.output_unit == 'msec':
         time_divisor = 1000.0
-    elif output_unit == 'usec':
+    elif args.output_unit == 'usec':
         time_divisor = 1.0
 
     # calculate response time interval associated with each histogram bucket
 
-    bucket_times = time_ranges(bucket_groups, buckets_per_group, fio_version=fio_version)
+    bucket_times = time_ranges(args.bucket_groups, buckets_per_group, fio_version=args.fio_version)
 
     # construct template for each histogram bucket array with buckets all zeroes
     # we just copy this for each new histogram
@@ -394,9 +359,9 @@ def compute_percentiles_from_logs():
     # print CSV header just like fiologparser_hist does
 
     header = 'msec, '
-    for p in pctiles_wanted:
+    for p in args.pctiles_wanted:
         header += '%3.1f, ' % p
-    print('time (millisec), percentiles in increasing order with values in ' + output_unit)
+    print('time (millisec), percentiles in increasing order with values in ' + args.output_unit)
     print(header)
 
     # parse the histogram logs
@@ -407,34 +372,34 @@ def compute_percentiles_from_logs():
 
     max_timestamp_all_logs = 0
     hist_files = {}
-    for fn in file_list:
+    for fn in args.file_list:
         try:
             (hist_files[fn], max_timestamp_ms)  = parse_hist_file(fn, buckets_per_interval)
         except FioHistoLogExc as e:
-            usage(str(e))
+            myabort(str(e))
         max_timestamp_all_logs = max(max_timestamp_all_logs, max_timestamp_ms)
 
-    (end_time, time_interval_count) = get_time_intervals(time_quantum, max_timestamp_all_logs)
-    all_threads_histograms = [ ((j*time_quantum*msec_per_sec), deepcopy(zeroed_buckets))
+    (end_time, time_interval_count) = get_time_intervals(args.time_quantum, max_timestamp_all_logs)
+    all_threads_histograms = [ ((j*args.time_quantum*msec_per_sec), deepcopy(zeroed_buckets))
                                 for j in range(0, time_interval_count) ]
 
     for logfn in hist_files.keys():
         aligned_per_thread = align_histo_log(hist_files[logfn], 
-                                             time_quantum, 
+                                             args.time_quantum, 
                                              buckets_per_interval, 
                                              max_timestamp_all_logs)
         for t in range(0, time_interval_count):
             (_, all_threads_histo_t) = all_threads_histograms[t]
             (_, log_histo_t) = aligned_per_thread[t]
-            pct = get_pctiles(log_histo_t, pctiles_wanted, bucket_times)
             add_to_histo_from( all_threads_histo_t, log_histo_t )
 
-    print('percentiles for entire set of threads')
+    # calculate percentiles across aggregate histogram for all threads
+
     for (t_msec, all_threads_histo_t) in all_threads_histograms:
         record = '%d, ' % t_msec
-        pct = get_pctiles(all_threads_histo_t, pctiles_wanted, bucket_times)
+        pct = get_pctiles(all_threads_histo_t, args.pctiles_wanted, bucket_times)
         if not pct:
-            for w in pctiles_wanted:
+            for w in args.pctiles_wanted:
                 record += ', '
         else:
             pct_keys = [ k for k in pct.keys() ]
@@ -644,7 +609,8 @@ class Test(unittest2.TestCase):
         self.A(time_ms1 == 0    and self.is_close(h1, expect1))
         self.A(time_ms2 == 5000 and self.is_close(h2, expect2))
 
-    def test_e1_get_pctiles(self):
+    # what to expect if histogram buckets are all equal
+    def test_e1_get_pctiles_flat_histo(self):
         with open(self.fn, 'w') as f:
             buckets = [ 100 for j in range(0, 128) ]
             f.write('9000, 1, 4096, %s\n' % ', '.join([str(b) for b in buckets]))
@@ -660,9 +626,27 @@ class Test(unittest2.TestCase):
         for (time_ms, histo) in aligned_log:
             pct_vs_time.append(get_pctiles(histo, pctiles_wanted, time_intervals))
         self.A(pct_vs_time[0] == None)  # no I/O in this time interval
-        expected_pctiles = { 0:0.001, 50:0.066, 100:0.256 }
+        expected_pctiles = { 0:0.000, 50:0.064, 100:0.256 }
         self.A(pct_vs_time[1] == expected_pctiles)
 
+    # what to expect if just the highest histogram bucket is used
+    def test_e2_get_pctiles_highest_pct(self):
+        fio_v3_bucket_count = 29 * 64
+        with open(self.fn, 'w') as f:
+            # make a empty fio v3 histogram
+            buckets = [ 0 for j in range(0, fio_v3_bucket_count) ]
+            # add one I/O request to last bucket
+            buckets[-1] = 1
+            f.write('9000, 1, 4096, %s\n' % ', '.join([str(b) for b in buckets]))
+        (raw_histo_log, max_timestamp_ms) = parse_hist_file(self.fn, fio_v3_bucket_count)
+        self.A(max_timestamp_ms == 9000)
+        aligned_log = align_histo_log(raw_histo_log, 5, fio_v3_bucket_count, max_timestamp_ms)
+        (time_ms, histo) = aligned_log[1]
+        time_intervals = time_ranges(29, 64)
+        expected_pctiles = { 100.0:(64*(1<<28))/1000.0 }
+        pct = get_pctiles( histo, [ 100.0 ], time_intervals )
+        self.A(pct == expected_pctiles)
+
 # we are using this module as a standalone program
 
 if __name__ == '__main__':