Make histogram samples non-cumulative by tracking a linked-list
[fio.git] / tools / hist / fiologparser_hist.py
... / ...
CommitLineData
1#!/usr/bin/env python2.7
2"""
3 Utility for converting *_clat_hist* files generated by fio into latency statistics.
4
5 Example usage:
6
7 $ fiologparser_hist.py *_clat_hist*
8 end-time, samples, min, avg, median, 90%, 95%, 99%, max
9 1000, 15, 192, 1678.107, 1788.859, 1856.076, 1880.040, 1899.208, 1888.000
10 2000, 43, 152, 1642.368, 1714.099, 1816.659, 1845.552, 1888.131, 1888.000
11 4000, 39, 1152, 1546.962, 1545.785, 1627.192, 1640.019, 1691.204, 1744
12 ...
13
14 Notes:
15
16 * end-times are calculated to be uniform increments of the --interval value given,
17 regardless of when histogram samples are reported. Of note:
18
19 * Intervals with no samples are omitted. In the example above this means
20 "no statistics from 2 to 3 seconds" and "39 samples influenced the statistics
21 of the interval from 3 to 4 seconds".
22
23 * Intervals with a single sample will have the same value for all statistics
24
25 * The number of samples is unweighted, corresponding to the total number of samples
26 which have any effect whatsoever on the interval.
27
28 * Min statistics are computed using value of the lower boundary of the first bin
29 (in increasing bin order) with non-zero samples in it. Similarly for max,
30 we take the upper boundary of the last bin with non-zero samples in it.
31 This is semantically identical to taking the 0th and 100th percentiles with a
32 50% bin-width buffer (because percentiles are computed using mid-points of
33 the bins). This enforces the following nice properties:
34
35 * min <= 50th <= 90th <= 95th <= 99th <= max
36
37 * min and max are strict lower and upper bounds on the actual
38 min / max seen by fio (and reported in *_clat.* with averaging turned off).
39
40 * Average statistics use a standard weighted arithmetic mean.
41
42 * Percentile statistics are computed using the weighted percentile method as
43 described here: https://en.wikipedia.org/wiki/Percentile#Weighted_percentile
44 See weights() method for details on how weights are computed for individual
45 samples. In process_interval() we further multiply by the height of each bin
46 to get weighted histograms.
47
48 * We convert files given on the command line, assumed to be fio histogram files,
49 An individual histogram file can contain the
50 histograms for multiple different r/w directions (notably when --rw=randrw). This
51 is accounted for by tracking each r/w direction separately. In the statistics
52 reported we ultimately merge *all* histograms (regardless of r/w direction).
53
54 * The value of *_GROUP_NR in stat.h (and *_BITS) determines how many latency bins
55 fio outputs when histogramming is enabled. Namely for the current default of
56 GROUP_NR=19, we get 1,216 bins with a maximum latency of approximately 17
57 seconds. For certain applications this may not be sufficient. With GROUP_NR=24
58 we have 1,536 bins, giving us a maximum latency of 541 seconds (~ 9 minutes). If
59 you expect your application to experience latencies greater than 17 seconds,
60 you will need to recompile fio with a larger GROUP_NR, e.g. with:
61
62 sed -i.bak 's/^#define FIO_IO_U_PLAT_GROUP_NR 19\n/#define FIO_IO_U_PLAT_GROUP_NR 24/g' stat.h
63 make fio
64
65 Quick reference table for the max latency corresponding to a sampling of
66 values for GROUP_NR:
67
68 GROUP_NR | # bins | max latency bin value
69 19 | 1216 | 16.9 sec
70 20 | 1280 | 33.8 sec
71 21 | 1344 | 67.6 sec
72 22 | 1408 | 2 min, 15 sec
73 23 | 1472 | 4 min, 32 sec
74 24 | 1536 | 9 min, 4 sec
75 25 | 1600 | 18 min, 8 sec
76 26 | 1664 | 36 min, 16 sec
77
78 * At present this program automatically detects the number of histogram bins in
79 the log files, and adjusts the bin latency values accordingly. In particular if
80 you use the --log_hist_coarseness parameter of fio, you get output files with
81 a number of bins according to the following table (note that the first
82 row is identical to the table above):
83
84 coarse \ GROUP_NR
85 19 20 21 22 23 24 25 26
86 -------------------------------------------------------
87 0 [[ 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664],
88 1 [ 608, 640, 672, 704, 736, 768, 800, 832],
89 2 [ 304, 320, 336, 352, 368, 384, 400, 416],
90 3 [ 152, 160, 168, 176, 184, 192, 200, 208],
91 4 [ 76, 80, 84, 88, 92, 96, 100, 104],
92 5 [ 38, 40, 42, 44, 46, 48, 50, 52],
93 6 [ 19, 20, 21, 22, 23, 24, 25, 26],
94 7 [ N/A, 10, N/A, 11, N/A, 12, N/A, 13],
95 8 [ N/A, 5, N/A, N/A, N/A, 6, N/A, N/A]]
96
97 For other values of GROUP_NR and coarseness, this table can be computed like this:
98
99 bins = [1216,1280,1344,1408,1472,1536,1600,1664]
100 max_coarse = 8
101 fncn = lambda z: list(map(lambda x: z/2**x if z % 2**x == 0 else nan, range(max_coarse + 1)))
102 np.transpose(list(map(fncn, bins)))
103
104 Also note that you can achieve the same downsampling / log file size reduction
105 by pre-processing (before inputting into this script) with half_bins.py.
106
107 * If you have not adjusted GROUP_NR for your (high latency) application, then you
108 will see the percentiles computed by this tool max out at the max latency bin
109 value as in the first table above, and in this plot (where GROUP_NR=19 and thus we see
110 a max latency of ~16.7 seconds in the red line):
111
112 https://www.cronburg.com/fio/max_latency_bin_value_bug.png
113
114 * Motivation for, design decisions, and the implementation process are
115 described in further detail here:
116
117 https://www.cronburg.com/fio/cloud-latency-problem-measurement/
118
119 @author Karl Cronburg <karl.cronburg@gmail.com>
120"""
121import os
122import sys
123import pandas
124import numpy as np
125
126err = sys.stderr.write
127
128def weighted_percentile(percs, vs, ws):
129 """ Use linear interpolation to calculate the weighted percentile.
130
131 Value and weight arrays are first sorted by value. The cumulative
132 distribution function (cdf) is then computed, after which np.interp
133 finds the two values closest to our desired weighted percentile(s)
134 and linearly interpolates them.
135
136 percs :: List of percentiles we want to calculate
137 vs :: Array of values we are computing the percentile of
138 ws :: Array of weights for our corresponding values
139 return :: Array of percentiles
140 """
141 idx = np.argsort(vs)
142 vs, ws = vs[idx], ws[idx] # weights and values sorted by value
143 cdf = 100 * (ws.cumsum() - ws / 2.0) / ws.sum()
144 return np.interp(percs, cdf, vs) # linear interpolation
145
146def weights(start_ts, end_ts, start, end):
147 """ Calculate weights based on fraction of sample falling in the
148 given interval [start,end]. Weights computed using vector / array
149 computation instead of for-loops.
150
151 Note that samples with zero time length are effectively ignored
152 (we set their weight to zero).
153
154 start_ts :: Array of start times for a set of samples
155 end_ts :: Array of end times for a set of samples
156 start :: int
157 end :: int
158 return :: Array of weights
159 """
160 sbounds = np.maximum(start_ts, start).astype(float)
161 ebounds = np.minimum(end_ts, end).astype(float)
162 ws = (ebounds - sbounds) / (end_ts - start_ts)
163 if np.any(np.isnan(ws)):
164 err("WARNING: zero-length sample(s) detected. Log file corrupt"
165 " / bad time values? Ignoring these samples.\n")
166 ws[np.where(np.isnan(ws))] = 0.0;
167 return ws
168
169def weighted_average(vs, ws):
170 return np.sum(vs * ws) / np.sum(ws)
171
172columns = ["end-time", "samples", "min", "avg", "median", "90%", "95%", "99%", "max"]
173percs = [50, 90, 95, 99]
174
175def fmt_float_list(ctx, num=1):
176 """ Return a comma separated list of float formatters to the required number
177 of decimal places. For instance:
178
179 fmt_float_list(ctx.decimals=4, num=3) == "%.4f, %.4f, %.4f"
180 """
181 return ', '.join(["%%.%df" % ctx.decimals] * num)
182
183# Default values - see beginning of main() for how we detect number columns in
184# the input files:
185__HIST_COLUMNS = 1216
186__NON_HIST_COLUMNS = 3
187__TOTAL_COLUMNS = __HIST_COLUMNS + __NON_HIST_COLUMNS
188
189def read_chunk(rdr, sz):
190 """ Read the next chunk of size sz from the given reader. """
191 try:
192 """ StopIteration occurs when the pandas reader is empty, and AttributeError
193 occurs if rdr is None due to the file being empty. """
194 new_arr = rdr.read().values
195 except (StopIteration, AttributeError):
196 return None
197
198 """ Extract array of just the times, and histograms matrix without times column. """
199 times, rws, szs = new_arr[:,0], new_arr[:,1], new_arr[:,2]
200 hists = new_arr[:,__NON_HIST_COLUMNS:]
201 times = times.reshape((len(times),1))
202 arr = np.append(times, hists, axis=1)
203
204 return arr
205
206def get_min(fps, arrs):
207 """ Find the file with the current first row with the smallest start time """
208 return min([fp for fp in fps if not arrs[fp] is None], key=lambda fp: arrs.get(fp)[0][0])
209
210def histogram_generator(ctx, fps, sz):
211
212 # Create a chunked pandas reader for each of the files:
213 rdrs = {}
214 for fp in fps:
215 try:
216 rdrs[fp] = pandas.read_csv(fp, dtype=int, header=None, chunksize=sz)
217 except ValueError as e:
218 if e.message == 'No columns to parse from file':
219 if not ctx.nowarn: sys.stderr.write("WARNING: Empty input file encountered.\n")
220 rdrs[fp] = None
221 else:
222 raise(e)
223
224 # Initial histograms from disk:
225 arrs = {fp: read_chunk(rdr, sz) for fp,rdr in rdrs.items()}
226 while True:
227
228 try:
229 """ ValueError occurs when nothing more to read """
230 fp = get_min(fps, arrs)
231 except ValueError:
232 return
233 arr = arrs[fp]
234 yield np.insert(arr[0], 1, fps.index(fp))
235 arrs[fp] = arr[1:]
236
237 if arrs[fp].shape[0] == 0:
238 arrs[fp] = read_chunk(rdrs[fp], sz)
239
240def _plat_idx_to_val(idx, edge=0.5, FIO_IO_U_PLAT_BITS=6, FIO_IO_U_PLAT_VAL=64):
241 """ Taken from fio's stat.c for calculating the latency value of a bin
242 from that bin's index.
243
244 idx : the value of the index into the histogram bins
245 edge : fractional value in the range [0,1]** indicating how far into
246 the bin we wish to compute the latency value of.
247
248 ** edge = 0.0 and 1.0 computes the lower and upper latency bounds
249 respectively of the given bin index. """
250
251 # MSB <= (FIO_IO_U_PLAT_BITS-1), cannot be rounded off. Use
252 # all bits of the sample as index
253 if (idx < (FIO_IO_U_PLAT_VAL << 1)):
254 return idx
255
256 # Find the group and compute the minimum value of that group
257 error_bits = (idx >> FIO_IO_U_PLAT_BITS) - 1
258 base = 1 << (error_bits + FIO_IO_U_PLAT_BITS)
259
260 # Find its bucket number of the group
261 k = idx % FIO_IO_U_PLAT_VAL
262
263 # Return the mean (if edge=0.5) of the range of the bucket
264 return base + ((k + edge) * (1 << error_bits))
265
266def plat_idx_to_val_coarse(idx, coarseness, edge=0.5):
267 """ Converts the given *coarse* index into a non-coarse index as used by fio
268 in stat.h:plat_idx_to_val(), subsequently computing the appropriate
269 latency value for that bin.
270 """
271
272 # Multiply the index by the power of 2 coarseness to get the bin
273 # bin index with a max of 1536 bins (FIO_IO_U_PLAT_GROUP_NR = 24 in stat.h)
274 stride = 1 << coarseness
275 idx = idx * stride
276 lower = _plat_idx_to_val(idx, edge=0.0)
277 upper = _plat_idx_to_val(idx + stride, edge=1.0)
278 return lower + (upper - lower) * edge
279
280def print_all_stats(ctx, end, mn, ss_cnt, vs, ws, mx):
281 ps = weighted_percentile(percs, vs, ws)
282
283 avg = weighted_average(vs, ws)
284 values = [mn, avg] + list(ps) + [mx]
285 row = [end, ss_cnt] + map(lambda x: float(x) / ctx.divisor, values)
286 fmt = "%d, %d, %d, " + fmt_float_list(ctx, 5) + ", %d"
287 print (fmt % tuple(row))
288
289def update_extreme(val, fncn, new_val):
290 """ Calculate min / max in the presence of None values """
291 if val is None: return new_val
292 else: return fncn(val, new_val)
293
294# See beginning of main() for how bin_vals are computed
295bin_vals = []
296lower_bin_vals = [] # lower edge of each bin
297upper_bin_vals = [] # upper edge of each bin
298
299def process_interval(ctx, samples, iStart, iEnd):
300 """ Construct the weighted histogram for the given interval by scanning
301 through all the histograms and figuring out which of their bins have
302 samples with latencies which overlap with the given interval
303 [iStart,iEnd].
304 """
305
306 times, files, hists = samples[:,0], samples[:,1], samples[:,2:]
307 iHist = np.zeros(__HIST_COLUMNS)
308 ss_cnt = 0 # number of samples affecting this interval
309 mn_bin_val, mx_bin_val = None, None
310
311 for end_time,file,hist in zip(times,files,hists):
312
313 # Only look at bins of the current histogram sample which
314 # started before the end of the current time interval [start,end]
315 start_times = (end_time - 0.5 * ctx.interval) - bin_vals / 1000.0
316 idx = np.where(start_times < iEnd)
317 s_ts, l_bvs, u_bvs, hs = start_times[idx], lower_bin_vals[idx], upper_bin_vals[idx], hist[idx]
318
319 # Increment current interval histogram by weighted values of future histogram:
320 ws = hs * weights(s_ts, end_time, iStart, iEnd)
321 iHist[idx] += ws
322
323 # Update total number of samples affecting current interval histogram:
324 ss_cnt += np.sum(hs)
325
326 # Update min and max bin values seen if necessary:
327 idx = np.where(hs != 0)[0]
328 if idx.size > 0:
329 mn_bin_val = update_extreme(mn_bin_val, min, l_bvs[max(0, idx[0] - 1)])
330 mx_bin_val = update_extreme(mx_bin_val, max, u_bvs[min(len(hs) - 1, idx[-1] + 1)])
331
332 if ss_cnt > 0: print_all_stats(ctx, iEnd, mn_bin_val, ss_cnt, bin_vals, iHist, mx_bin_val)
333
334def guess_max_from_bins(ctx, hist_cols):
335 """ Try to guess the GROUP_NR from given # of histogram
336 columns seen in an input file """
337 max_coarse = 8
338 if ctx.group_nr < 19 or ctx.group_nr > 26:
339 bins = [ctx.group_nr * (1 << 6)]
340 else:
341 bins = [1216,1280,1344,1408,1472,1536,1600,1664]
342 coarses = range(max_coarse + 1)
343 fncn = lambda z: list(map(lambda x: z/2**x if z % 2**x == 0 else -10, coarses))
344
345 arr = np.transpose(list(map(fncn, bins)))
346 idx = np.where(arr == hist_cols)
347 if len(idx[1]) == 0:
348 table = repr(arr.astype(int)).replace('-10', 'N/A').replace('array',' ')
349 err("Unable to determine bin values from input clat_hist files. Namely \n"
350 "the first line of file '%s' " % ctx.FILE[0] + "has %d \n" % (__TOTAL_COLUMNS,) +
351 "columns of which we assume %d " % (hist_cols,) + "correspond to histogram bins. \n"
352 "This number needs to be equal to one of the following numbers:\n\n"
353 + table + "\n\n"
354 "Possible reasons and corresponding solutions:\n"
355 " - Input file(s) does not contain histograms.\n"
356 " - You recompiled fio with a different GROUP_NR. If so please specify this\n"
357 " new GROUP_NR on the command line with --group_nr\n")
358 exit(1)
359 return bins[idx[1][0]]
360
361def main(ctx):
362
363 # Automatically detect how many columns are in the input files,
364 # calculate the corresponding 'coarseness' parameter used to generate
365 # those files, and calculate the appropriate bin latency values:
366 with open(ctx.FILE[0], 'r') as fp:
367 global bin_vals,lower_bin_vals,upper_bin_vals,__HIST_COLUMNS,__TOTAL_COLUMNS
368 __TOTAL_COLUMNS = len(fp.readline().split(','))
369 __HIST_COLUMNS = __TOTAL_COLUMNS - __NON_HIST_COLUMNS
370
371 max_cols = guess_max_from_bins(ctx, __HIST_COLUMNS)
372 coarseness = int(np.log2(float(max_cols) / __HIST_COLUMNS))
373 bin_vals = np.array(map(lambda x: plat_idx_to_val_coarse(x, coarseness), np.arange(__HIST_COLUMNS)), dtype=float)
374 lower_bin_vals = np.array(map(lambda x: plat_idx_to_val_coarse(x, coarseness, 0.0), np.arange(__HIST_COLUMNS)), dtype=float)
375 upper_bin_vals = np.array(map(lambda x: plat_idx_to_val_coarse(x, coarseness, 1.0), np.arange(__HIST_COLUMNS)), dtype=float)
376
377 fps = [open(f, 'r') for f in ctx.FILE]
378 gen = histogram_generator(ctx, fps, ctx.buff_size)
379
380 print(', '.join(columns))
381
382 try:
383 start, end = 0, ctx.interval
384 arr = np.empty(shape=(0,__TOTAL_COLUMNS - 1))
385 more_data = True
386 while more_data or len(arr) > 0:
387
388 # Read up to ctx.max_latency (default 20 seconds) of data from end of current interval.
389 while len(arr) == 0 or arr[-1][0] < ctx.max_latency * 1000 + end:
390 try:
391 new_arr = next(gen)
392 except StopIteration:
393 more_data = False
394 break
395 arr = np.append(arr, new_arr.reshape((1,__TOTAL_COLUMNS - 1)), axis=0)
396 arr = arr.astype(int)
397
398 if arr.size > 0:
399 process_interval(ctx, arr, start, end)
400
401 # Update arr to throw away samples we no longer need - samples which
402 # end before the start of the next interval, i.e. the end of the
403 # current interval:
404 idx = np.where(arr[:,0] > end)
405 arr = arr[idx]
406
407 start += ctx.interval
408 end = start + ctx.interval
409 finally:
410 map(lambda f: f.close(), fps)
411
412
413if __name__ == '__main__':
414 import argparse
415 p = argparse.ArgumentParser()
416 arg = p.add_argument
417 arg("FILE", help='space separated list of latency log filenames', nargs='+')
418 arg('--buff_size',
419 default=10000,
420 type=int,
421 help='number of samples to buffer into numpy at a time')
422
423 arg('--max_latency',
424 default=20,
425 type=float,
426 help='number of seconds of data to process at a time')
427
428 arg('-i', '--interval',
429 default=1000,
430 type=int,
431 help='interval width (ms)')
432
433 arg('-d', '--divisor',
434 required=False,
435 type=int,
436 default=1,
437 help='divide the results by this value.')
438
439 arg('--decimals',
440 default=3,
441 type=int,
442 help='number of decimal places to print floats to')
443
444 arg('--nowarn',
445 dest='nowarn',
446 action='store_false',
447 default=True,
448 help='do not print any warning messages to stderr')
449
450 arg('--group_nr',
451 default=19,
452 type=int,
453 help='FIO_IO_U_PLAT_GROUP_NR as defined in stat.h')
454
455 main(p.parse_args())
456