blob: f27491b34dc37321c3511f2972b4a464128b73c6 [file] [log] [blame]
import os
import csv
import bisect
import logging
import itertools
import collections
from cStringIO import StringIO
try:
import numpy
import scipy
import matplotlib
matplotlib.use('svg')
import matplotlib.pyplot as plt
except ImportError:
plt = None
import wally
from wally.utils import ssize2b
from wally.statistic import round_3_digit
from wally.suits.io.fio_task_parser import (get_test_sync_mode,
get_test_summary,
parse_all_in_1,
abbv_name_to_full)
logger = logging.getLogger("wally.report")
class DiskInfo(object):
def __init__(self):
self.direct_iops_r_max = 0
self.direct_iops_w_max = 0
# 64 used instead of 4k to faster feed caches
self.direct_iops_w64_max = 0
self.rws4k_10ms = 0
self.rws4k_30ms = 0
self.rws4k_100ms = 0
self.bw_write_max = 0
self.bw_read_max = 0
report_funcs = []
class Attrmapper(object):
def __init__(self, dct):
self.__dct = dct
def __getattr__(self, name):
try:
return self.__dct[name]
except KeyError:
raise AttributeError(name)
class PerfInfo(object):
def __init__(self, name, summary, intervals, params, testnodes_count):
self.name = name
self.bw = None
self.iops = None
self.lat = None
self.lat_50 = None
self.lat_95 = None
self.raw_bw = []
self.raw_iops = []
self.raw_lat = []
self.params = params
self.intervals = intervals
self.testnodes_count = testnodes_count
self.summary = summary
self.p = Attrmapper(self.params.vals)
self.sync_mode = get_test_sync_mode(self.params)
self.concurence = self.params.vals.get('numjobs', 1)
# disk_info = None
# base = None
# linearity = None
def group_by_name(test_data):
name_map = collections.defaultdict(lambda: [])
for data in test_data:
name_map[(data.name, data.summary())].append(data)
return name_map
def report(name, required_fields):
def closure(func):
report_funcs.append((required_fields.split(","), name, func))
return func
return closure
def get_test_lcheck_params(pinfo):
res = [{
's': 'sync',
'd': 'direct',
'a': 'async',
'x': 'sync direct'
}[pinfo.sync_mode]]
res.append(pinfo.p.rw)
return " ".join(res)
def get_emb_data_svg(plt):
sio = StringIO()
plt.savefig(sio, format='svg')
img_start = "<!-- Created with matplotlib (http://matplotlib.org/) -->"
return sio.getvalue().split(img_start, 1)[1]
def get_template(templ_name):
very_root_dir = os.path.dirname(os.path.dirname(wally.__file__))
templ_dir = os.path.join(very_root_dir, 'report_templates')
templ_file = os.path.join(templ_dir, templ_name)
return open(templ_file, 'r').read()
def group_by(data, func):
if len(data) < 2:
yield data
return
ndata = [(func(dt), dt) for dt in data]
ndata.sort(key=func)
pkey, dt = ndata[0]
curr_list = [dt]
for key, val in ndata[1:]:
if pkey != key:
yield curr_list
curr_list = [val]
else:
curr_list.append(val)
pkey = key
yield curr_list
@report('linearity', 'linearity_test')
def linearity_report(processed_results, lab_info, comment):
labels_and_data_mp = collections.defaultdict(lambda: [])
vls = {}
# plot io_time = func(bsize)
for res in processed_results.values():
if res.name.startswith('linearity_test'):
iotimes = [1000. / val for val in res.iops.raw]
op_summ = get_test_summary(res.params)[:3]
labels_and_data_mp[op_summ].append(
[res.p.blocksize, res.iops.raw, iotimes])
cvls = res.params.vals.copy()
del cvls['blocksize']
del cvls['rw']
cvls.pop('sync', None)
cvls.pop('direct', None)
cvls.pop('buffered', None)
if op_summ not in vls:
vls[op_summ] = cvls
else:
assert cvls == vls[op_summ]
all_labels = None
_, ax1 = plt.subplots()
for name, labels_and_data in labels_and_data_mp.items():
labels_and_data.sort(key=lambda x: ssize2b(x[0]))
labels, _, iotimes = zip(*labels_and_data)
if all_labels is None:
all_labels = labels
else:
assert all_labels == labels
plt.boxplot(iotimes)
if len(labels_and_data) > 2 and \
ssize2b(labels_and_data[-2][0]) >= 4096:
xt = range(1, len(labels) + 1)
def io_time(sz, bw, initial_lat):
return sz / bw + initial_lat
x = numpy.array(map(ssize2b, labels))
y = numpy.array([sum(dt) / len(dt) for dt in iotimes])
popt, _ = scipy.optimize.curve_fit(io_time, x, y, p0=(100., 1.))
y1 = io_time(x, *popt)
plt.plot(xt, y1, linestyle='--',
label=name + ' LS linear approx')
for idx, (sz, _, _) in enumerate(labels_and_data):
if ssize2b(sz) >= 4096:
break
bw = (x[-1] - x[idx]) / (y[-1] - y[idx])
lat = y[-1] - x[-1] / bw
y2 = io_time(x, bw, lat)
plt.plot(xt, y2, linestyle='--',
label=abbv_name_to_full(name) +
' (4k & max) linear approx')
plt.setp(ax1, xticklabels=labels)
plt.xlabel("Block size")
plt.ylabel("IO time, ms")
plt.subplots_adjust(top=0.85)
plt.legend(bbox_to_anchor=(0.5, 1.15),
loc='upper center',
prop={'size': 10}, ncol=2)
plt.grid()
iotime_plot = get_emb_data_svg(plt)
plt.clf()
# plot IOPS = func(bsize)
_, ax1 = plt.subplots()
for name, labels_and_data in labels_and_data_mp.items():
labels_and_data.sort(key=lambda x: ssize2b(x[0]))
_, data, _ = zip(*labels_and_data)
plt.boxplot(data)
avg = [float(sum(arr)) / len(arr) for arr in data]
xt = range(1, len(data) + 1)
plt.plot(xt, avg, linestyle='--',
label=abbv_name_to_full(name) + " avg")
plt.setp(ax1, xticklabels=labels)
plt.xlabel("Block size")
plt.ylabel("IOPS")
plt.legend(bbox_to_anchor=(0.5, 1.15),
loc='upper center',
prop={'size': 10}, ncol=2)
plt.grid()
plt.subplots_adjust(top=0.85)
iops_plot = get_emb_data_svg(plt)
res = set(get_test_lcheck_params(res) for res in processed_results.values())
ncount = list(set(res.testnodes_count for res in processed_results.values()))
conc = list(set(res.concurence for res in processed_results.values()))
assert len(conc) == 1
assert len(ncount) == 1
descr = {
'vm_count': ncount[0],
'concurence': conc[0],
'oper_descr': ", ".join(res).capitalize()
}
params_map = {'iotime_vs_size': iotime_plot,
'iops_vs_size': iops_plot,
'descr': descr}
return get_template('report_linearity.html').format(**params_map)
@report('lat_vs_iops', 'lat_vs_iops')
def lat_vs_iops(processed_results, lab_info, comment):
lat_iops = collections.defaultdict(lambda: [])
requsted_vs_real = collections.defaultdict(lambda: {})
for res in processed_results.values():
if res.name.startswith('lat_vs_iops'):
lat_iops[res.concurence].append((res.lat,
0,
res.iops.average,
res.iops.deviation))
# lat_iops[res.concurence].append((res.lat.average / 1000.0,
# res.lat.deviation / 1000.0,
# res.iops.average,
# res.iops.deviation))
requested_iops = res.p.rate_iops * res.concurence
requsted_vs_real[res.concurence][requested_iops] = \
(res.iops.average, res.iops.deviation)
colors = ['red', 'green', 'blue', 'orange', 'magenta', "teal"]
colors_it = iter(colors)
for conc, lat_iops in sorted(lat_iops.items()):
lat, dev, iops, iops_dev = zip(*lat_iops)
plt.errorbar(iops, lat, xerr=iops_dev, yerr=dev, fmt='ro',
label=str(conc) + " threads",
color=next(colors_it))
plt.xlabel("IOPS")
plt.ylabel("Latency, ms")
plt.grid()
plt.legend(loc=0)
plt_iops_vs_lat = get_emb_data_svg(plt)
plt.clf()
colors_it = iter(colors)
for conc, req_vs_real in sorted(requsted_vs_real.items()):
req, real = zip(*sorted(req_vs_real.items()))
iops, dev = zip(*real)
plt.errorbar(req, iops, yerr=dev, fmt='ro',
label=str(conc) + " threads",
color=next(colors_it))
plt.xlabel("Requested IOPS")
plt.ylabel("Get IOPS")
plt.grid()
plt.legend(loc=0)
plt_iops_vs_requested = get_emb_data_svg(plt)
res1 = processed_results.values()[0]
params_map = {'iops_vs_lat': plt_iops_vs_lat,
'iops_vs_requested': plt_iops_vs_requested,
'oper_descr': get_test_lcheck_params(res1).capitalize()}
return get_template('report_iops_vs_lat.html').format(**params_map)
def render_all_html(comment, info, lab_description, images, templ_name):
data = info.__dict__.copy()
for name, val in data.items():
if not name.startswith('__'):
if val is None:
if name in ('direct_iops_w64_max', 'direct_iops_w_max'):
data[name] = ('-', '-', '-')
else:
data[name] = '-'
elif isinstance(val, (int, float, long)):
data[name] = round_3_digit(val)
data['bw_read_max'] = (data['bw_read_max'][0] // 1024,
data['bw_read_max'][1],
data['bw_read_max'][2])
data['bw_write_max'] = (data['bw_write_max'][0] // 1024,
data['bw_write_max'][1],
data['bw_write_max'][2])
# templ_name = 'report_ceph_1.html'
# import pprint
# pprint.pprint(data)
# pprint.pprint(info.__dict__)
images.update(data)
templ = get_template(templ_name)
return templ.format(lab_info=lab_description,
comment=comment,
**images)
def io_chart(title, concurence,
latv, latv_min, latv_max,
iops_or_bw, iops_or_bw_err,
legend,
log_iops=False,
log_lat=False,
boxplots=False,
latv_50=None,
latv_95=None,
error2=None):
matplotlib.rcParams.update({'font.size': 10})
points = " MiBps" if legend == 'BW' else ""
lc = len(concurence)
width = 0.35
xt = range(1, lc + 1)
op_per_vm = [v / (vm * th) for v, (vm, th) in zip(iops_or_bw, concurence)]
fig, p1 = plt.subplots()
xpos = [i - width / 2 for i in xt]
p1.bar(xpos, iops_or_bw,
width=width,
color='y',
label=legend)
err1_leg = None
for pos, y, err in zip(xpos, iops_or_bw, iops_or_bw_err):
err1_leg = p1.errorbar(pos + width / 2,
y,
err,
color='magenta')
err2_leg = None
if error2 is not None:
for pos, y, err in zip(xpos, iops_or_bw, error2):
err2_leg = p1.errorbar(pos + width / 2 + 0.08,
y,
err,
lw=2,
alpha=0.5,
color='teal')
p1.grid(True)
p1.plot(xt, op_per_vm, '--', label=legend + "/thread", color='black')
handles1, labels1 = p1.get_legend_handles_labels()
handles1 += [err1_leg]
labels1 += ["95% conf"]
if err2_leg is not None:
handles1 += [err2_leg]
labels1 += ["95% dev"]
p2 = p1.twinx()
if latv_50 is None:
p2.plot(xt, latv_max, label="lat max")
p2.plot(xt, latv, label="lat avg")
p2.plot(xt, latv_min, label="lat min")
else:
p2.plot(xt, latv_50, label="lat med")
p2.plot(xt, latv_95, label="lat 95%")
plt.xlim(0.5, lc + 0.5)
plt.xticks(xt, ["{0} * {1}".format(vm, th) for (vm, th) in concurence])
p1.set_xlabel("VM Count * Thread per VM")
p1.set_ylabel(legend + points)
p2.set_ylabel("Latency ms")
plt.title(title)
handles2, labels2 = p2.get_legend_handles_labels()
plt.legend(handles1 + handles2, labels1 + labels2,
loc='center left', bbox_to_anchor=(1.1, 0.81))
if log_iops:
p1.set_yscale('log')
if log_lat:
p2.set_yscale('log')
plt.subplots_adjust(right=0.68)
return get_emb_data_svg(plt)
def make_plots(processed_results, plots):
"""
processed_results: [PerfInfo]
plots = [(test_name_prefix:str, fname:str, description:str)]
"""
files = {}
for name_pref, fname, desc in plots:
chart_data = []
for res in processed_results:
summ = res.name + "_" + res.summary
if summ.startswith(name_pref):
chart_data.append(res)
if len(chart_data) == 0:
raise ValueError("Can't found any date for " + name_pref)
use_bw = ssize2b(chart_data[0].p.blocksize) > 16 * 1024
chart_data.sort(key=lambda x: x.params['vals']['numjobs'])
lat = None
lat_min = None
lat_max = None
lat_50 = [x.lat_50 for x in chart_data]
lat_95 = [x.lat_95 for x in chart_data]
lat_diff_max = max(x.lat_95 / x.lat_50 for x in chart_data)
lat_log_scale = (lat_diff_max > 10)
testnodes_count = x.testnodes_count
concurence = [(testnodes_count, x.concurence)
for x in chart_data]
if use_bw:
data = [x.bw.average / 1000 for x in chart_data]
data_conf = [x.bw.confidence / 1000 for x in chart_data]
data_dev = [x.bw.deviation * 2.5 / 1000 for x in chart_data]
name = "BW"
else:
data = [x.iops.average for x in chart_data]
data_conf = [x.iops.confidence for x in chart_data]
data_dev = [x.iops.deviation * 2 for x in chart_data]
name = "IOPS"
fc = io_chart(title=desc,
concurence=concurence,
latv=lat,
latv_min=lat_min,
latv_max=lat_max,
iops_or_bw=data,
iops_or_bw_err=data_conf,
legend=name,
log_lat=lat_log_scale,
latv_50=lat_50,
latv_95=lat_95,
error2=data_dev)
files[fname] = fc
return files
def find_max_where(processed_results, sync_mode, blocksize, rw, iops=True):
result = None
attr = 'iops' if iops else 'bw'
for measurement in processed_results:
ok = measurement.sync_mode == sync_mode
ok = ok and (measurement.p.blocksize == blocksize)
ok = ok and (measurement.p.rw == rw)
if ok:
field = getattr(measurement, attr)
if result is None:
result = field
elif field.average > result.average:
result = field
return result
def get_disk_info(processed_results):
di = DiskInfo()
di.direct_iops_w_max = find_max_where(processed_results,
'd', '4k', 'randwrite')
di.direct_iops_r_max = find_max_where(processed_results,
'd', '4k', 'randread')
di.direct_iops_w64_max = find_max_where(processed_results,
'd', '64k', 'randwrite')
for sz in ('16m', '64m'):
di.bw_write_max = find_max_where(processed_results,
'd', sz, 'randwrite', False)
if di.bw_write_max is not None:
break
if di.bw_write_max is None:
for sz in ('1m', '2m', '4m', '8m'):
di.bw_write_max = find_max_where(processed_results,
'd', sz, 'write', False)
if di.bw_write_max is not None:
break
for sz in ('16m', '64m'):
di.bw_read_max = find_max_where(processed_results,
'd', sz, 'randread', False)
if di.bw_read_max is not None:
break
if di.bw_read_max is None:
di.bw_read_max = find_max_where(processed_results,
'd', '1m', 'read', False)
rws4k_iops_lat_th = []
for res in processed_results:
if res.sync_mode in 'xs' and res.p.blocksize == '4k':
if res.p.rw != 'randwrite':
continue
rws4k_iops_lat_th.append((res.iops.average,
res.lat,
# res.lat.average,
res.concurence))
rws4k_iops_lat_th.sort(key=lambda (_1, _2, conc): conc)
latv = [lat for _, lat, _ in rws4k_iops_lat_th]
for tlat in [10, 30, 100]:
pos = bisect.bisect_left(latv, tlat)
if 0 == pos:
setattr(di, 'rws4k_{}ms'.format(tlat), 0)
elif pos == len(latv):
iops3, _, _ = rws4k_iops_lat_th[-1]
iops3 = int(round_3_digit(iops3))
setattr(di, 'rws4k_{}ms'.format(tlat), ">=" + str(iops3))
else:
lat1 = latv[pos - 1]
lat2 = latv[pos]
iops1, _, th1 = rws4k_iops_lat_th[pos - 1]
iops2, _, th2 = rws4k_iops_lat_th[pos]
th_lat_coef = (th2 - th1) / (lat2 - lat1)
th3 = th_lat_coef * (tlat - lat1) + th1
th_iops_coef = (iops2 - iops1) / (th2 - th1)
iops3 = th_iops_coef * (th3 - th1) + iops1
iops3 = int(round_3_digit(iops3))
setattr(di, 'rws4k_{}ms'.format(tlat), iops3)
hdi = DiskInfo()
def pp(x):
med, conf = x.rounded_average_conf()
conf_perc = int(float(conf) / med * 100)
dev_perc = int(float(x.deviation) / med * 100)
return (round_3_digit(med), conf_perc, dev_perc)
hdi.direct_iops_r_max = pp(di.direct_iops_r_max)
if di.direct_iops_w_max is not None:
hdi.direct_iops_w_max = pp(di.direct_iops_w_max)
else:
hdi.direct_iops_w_max = None
if di.direct_iops_w64_max is not None:
hdi.direct_iops_w64_max = pp(di.direct_iops_w64_max)
else:
hdi.direct_iops_w64_max = None
hdi.bw_write_max = pp(di.bw_write_max)
hdi.bw_read_max = pp(di.bw_read_max)
hdi.rws4k_10ms = di.rws4k_10ms if 0 != di.rws4k_10ms else None
hdi.rws4k_30ms = di.rws4k_30ms if 0 != di.rws4k_30ms else None
hdi.rws4k_100ms = di.rws4k_100ms if 0 != di.rws4k_100ms else None
return hdi
@report('hdd', 'hdd')
def make_hdd_report(processed_results, lab_info, comment):
plots = [
('hdd_rrd4k', 'rand_read_4k', 'Random read 4k direct IOPS'),
('hdd_rwx4k', 'rand_write_4k', 'Random write 4k sync IOPS')
]
perf_infos = [res.disk_perf_info() for res in processed_results]
images = make_plots(perf_infos, plots)
di = get_disk_info(perf_infos)
return render_all_html(comment, di, lab_info, images, "report_hdd.html")
@report('cinder_iscsi', 'cinder_iscsi')
def make_cinder_iscsi_report(processed_results, lab_info, comment):
plots = [
('cinder_iscsi_rrd4k', 'rand_read_4k', 'Random read 4k direct IOPS'),
('cinder_iscsi_rwx4k', 'rand_write_4k', 'Random write 4k sync IOPS')
]
perf_infos = [res.disk_perf_info() for res in processed_results]
try:
images = make_plots(perf_infos, plots)
except ValueError:
plots = [
('cinder_iscsi_rrd4k', 'rand_read_4k', 'Random read 4k direct IOPS'),
('cinder_iscsi_rws4k', 'rand_write_4k', 'Random write 4k sync IOPS')
]
images = make_plots(perf_infos, plots)
di = get_disk_info(perf_infos)
return render_all_html(comment, di, lab_info, images, "report_cinder_iscsi.html")
@report('ceph', 'ceph')
def make_ceph_report(processed_results, lab_info, comment):
plots = [
('ceph_rrd4k', 'rand_read_4k', 'Random read 4k direct IOPS'),
('ceph_rws4k', 'rand_write_4k', 'Random write 4k sync IOPS'),
('ceph_rrd16m', 'rand_read_16m', 'Random read 16m direct MiBps'),
('ceph_rwd16m', 'rand_write_16m',
'Random write 16m direct MiBps'),
]
perf_infos = [res.disk_perf_info() for res in processed_results]
images = make_plots(perf_infos, plots)
di = get_disk_info(perf_infos)
return render_all_html(comment, di, lab_info, images, "report_ceph.html")
@report('mixed', 'mixed')
def make_mixed_report(processed_results, lab_info, comment):
#
# IOPS(X% read) = 100 / ( X / IOPS_W + (100 - X) / IOPS_R )
#
perf_infos = [res.disk_perf_info() for res in processed_results]
mixed = collections.defaultdict(lambda: [])
is_ssd = False
for res in perf_infos:
if res.name.startswith('mixed'):
if res.name.startswith('mixed-ssd'):
is_ssd = True
mixed[res.concurence].append((res.p.rwmixread,
res.lat,
0,
# res.lat.average / 1000.0,
# res.lat.deviation / 1000.0,
res.iops.average,
res.iops.deviation))
if len(mixed) == 0:
raise ValueError("No mixed load found")
fig, p1 = plt.subplots()
p2 = p1.twinx()
colors = ['red', 'green', 'blue', 'orange', 'magenta', "teal"]
colors_it = iter(colors)
for conc, mix_lat_iops in sorted(mixed.items()):
mix_lat_iops = sorted(mix_lat_iops)
read_perc, lat, dev, iops, iops_dev = zip(*mix_lat_iops)
p1.errorbar(read_perc, iops, color=next(colors_it),
yerr=iops_dev, label=str(conc) + " th")
p2.errorbar(read_perc, lat, color=next(colors_it),
ls='--', yerr=dev, label=str(conc) + " th lat")
if is_ssd:
p1.set_yscale('log')
p2.set_yscale('log')
p1.set_xlim(-5, 105)
read_perc = set(read_perc)
read_perc.add(0)
read_perc.add(100)
read_perc = sorted(read_perc)
plt.xticks(read_perc, map(str, read_perc))
p1.grid(True)
p1.set_xlabel("% of reads")
p1.set_ylabel("Mixed IOPS")
p2.set_ylabel("Latency, ms")
handles1, labels1 = p1.get_legend_handles_labels()
handles2, labels2 = p2.get_legend_handles_labels()
plt.subplots_adjust(top=0.85)
plt.legend(handles1 + handles2, labels1 + labels2,
bbox_to_anchor=(0.5, 1.15),
loc='upper center',
prop={'size': 12}, ncol=3)
plt.show()
def make_load_report(idx, results_dir, fname):
dpath = os.path.join(results_dir, "io_" + str(idx))
files = sorted(os.listdir(dpath))
gf = lambda x: "_".join(x.rsplit(".", 1)[0].split('_')[:3])
for key, group in itertools.groupby(files, gf):
fname = os.path.join(dpath, key + ".fio")
cfgs = list(parse_all_in_1(open(fname).read(), fname))
fname = os.path.join(dpath, key + "_lat.log")
curr = []
arrays = []
with open(fname) as fd:
for offset, lat, _, _ in csv.reader(fd):
offset = int(offset)
lat = int(lat)
if len(curr) > 0 and curr[-1][0] > offset:
arrays.append(curr)
curr = []
curr.append((offset, lat))
arrays.append(curr)
conc = int(cfgs[0].vals.get('numjobs', 1))
if conc != 5:
continue
assert len(arrays) == len(cfgs) * conc
garrays = [[(0, 0)] for _ in range(conc)]
for offset in range(len(cfgs)):
for acc, new_arr in zip(garrays, arrays[offset * conc:(offset + 1) * conc]):
last = acc[-1][0]
for off, lat in new_arr:
acc.append((off / 1000. + last, lat / 1000.))
for cfg, arr in zip(cfgs, garrays):
plt.plot(*zip(*arr[1:]))
plt.show()
exit(1)
def make_io_report(dinfo, comment, path, lab_info=None):
lab_info = {
"total_disk": "None",
"total_memory": "None",
"nodes_count": "None",
"processor_count": "None"
}
try:
res_fields = sorted(v.name for v in dinfo)
found = False
for fields, name, func in report_funcs:
for field in fields:
pos = bisect.bisect_left(res_fields, field)
if pos == len(res_fields):
break
if not res_fields[pos].startswith(field):
break
else:
found = True
hpath = path.format(name)
try:
report = func(dinfo, lab_info, comment)
except:
logger.exception("Diring {0} report generation".format(name))
continue
if report is not None:
try:
with open(hpath, "w") as fd:
fd.write(report)
except:
logger.exception("Diring saving {0} report".format(name))
continue
logger.info("Report {0} saved into {1}".format(name, hpath))
else:
logger.warning("No report produced by {0!r}".format(name))
if not found:
logger.warning("No report generator found for this load")
except Exception as exc:
import traceback
traceback.print_exc()
logger.error("Failed to generate html report:" + str(exc))