Commit 916782b6 authored by Kirill Smelkov's avatar Kirill Smelkov

X normalize/convert units, so that disk and ping/tcp latencies could be plotted too

parent 89682f01
......@@ -58,7 +58,7 @@ class Measure(object):
# Unit is a symbolic unit, like "ns/op", "µs/object" or "L1-miss-ns/op"
class Unit(object):
def __init__(self, unit):
self.unit = unit # XXX normalize e.g. "µs" and "ns" to "time"
self.unit = unit
# eq, hash - so that Unit can be used as key in dict or set
def __eq__(self, other):
......@@ -89,6 +89,13 @@ class Stats(object):
# ----------------------------------------
# method decorator allows to define methods separate from class.
def method(cls):
def deco(f):
setattr(cls, f.func_name, f)
return deco
_sp_re = re.compile(r'\s')
# parse_label tries to parse line as label.
......@@ -130,6 +137,7 @@ def parse_benchline(line):
try:
return _parse_benchline(linev)
except Exception:
# FIXME -> more fine-grained catch, not to skip programming mistakes
return None
def _parse_benchline(linev):
......@@ -152,11 +160,55 @@ def _parse_benchline(linev):
value = float(value)
unit = Unit(unit)
measurev.append(Measure(value, unit))
unit, scale = unit.normalize()
measurev.append(Measure(value*scale, unit))
return BenchLine(name, niter, measurev)
# normalize converts unit into normalized Unit & scale.
#
# It returns base unit and scaling factor to convert values from original
# unit to base one.
#
# For example
#
# "µs" -> "s", 1E-6
# "ms/op" -> "s/op", 1E-3
# "user-s/op" -> "user-s/op", 1
#
# returns -> Unit, scale.
@method(Unit)
def normalize(self):
# split unit string into prefix and just unit
unit = self.unit
unitv = unit.rsplit('-', 1) # "L1-miss-ns/op" -> "L1-miss-", "ns/op"
unit = unitv[-1]
# unit -> nom/denom/... ; scale nom to base
fracv = unit.split('/', 1)
rescale = 1
_ = _unitTab.get(fracv[0])
if _ is not None:
fracv[0] = _[0]
rescale = _[1]
# rebuild whole unit string
unitv[-1] = '/'.join(fracv)
unit = '-'.join(unitv)
return Unit(unit), rescale
# {} unit -> (base_unit, rescale)
_unitTab = {
"ns": ("s", 1E-9),
u"µs": ("s", 1E-6),
"us": ("s", 1E-6),
"ms": ("s", 1E-3),
}
# load loads benchmark data from a reader.
#
# r is required to implement `.readlines()`.
......@@ -276,13 +328,6 @@ def xload_file(path):
# method decorator allows to define methods separate from class.
def method(cls):
def deco(f):
setattr(cls, f.func_name, f)
return deco
# bylabel splits Benchmark into several groups of Benchmarks with specified
# labels having same values across a given group.
#
......@@ -351,6 +396,30 @@ def byunit(self):
return byunit
# convert_unit converts benchmark values to be in specified unit.
#
# all original values must have units compatible with the conversion.
#
# returns -> new Benchmark with converted units.
@method(Benchmark)
def convert_unit(self, unit):
B = Benchmark()
U = Unit(unit)
u, uscale = U.normalize()
for b in self:
measurev = []
for m in b.measurev:
mu, muscale = m.unit.normalize()
if mu != u:
raise ValueError('convert unit: units are not convertible: (%s, %s)' % (unit, m.unit))
measurev.append(Measure(m.value * muscale / uscale, U))
B.append(BenchLine(b.name, b.niter, measurev, b.labels))
return B
# stats returns statistics about values in benchmark collection.
#
......
......@@ -212,12 +212,12 @@ def Bmerge(B, merger):
for name_ in name_v:
b_ = Bname.get(name_)
if b_ is not None:
# ok to merge if either probably same or the difference is < 1µs # XXX 0.5
# ok to merge if either probably same or the difference is < 0.5µs
s_ = b_.stats()
assert s.unit == s_.unit, (s.unit, s_.unit)
t = ttest_ind_from_stats(s.avg, s.std, s.ninliers, s_.avg, s_.std, s_.ninliers)
if t.pvalue >= 0.3 or (s.unit == usop and abs(s.avg - s_.avg) < 0.5):
print 'merging %s (%s)\t+ %s (%s) (%s)' % (name, s, name_, s_, t)
#print 'merging %s (%s)\t+ %s (%s) (%s)' % (name, s, name_, s_, t)
bmerge.extend(b_)
namev.append(name_)
......@@ -306,6 +306,9 @@ _stylefor_re = [(re.compile(_), sty) for _,sty in [
(r'unzlib/go/wczdata', _(color='C7', ls='dashed')),
(r'unzlib/(py|\*)/prod1-avg', _(color='C8')),
(r'unzlib/go/prod1-avg', _(color='C8', ls='dashed')),
(r'disk/randread/direct/4K-avg', _(color='C9')),
(r'disk/randread/pagecache/4K-avg', _(color='C9', ls='dashed')),
]]
del _
......@@ -424,7 +427,7 @@ def plotlat1(ax, S):
# plotnode1 plots latencies of base CPU/disk operations on the nodes.
def plotnode1(ax, B, w=0.05, details=False):
Bu = B.byunit()
Bn = Bu[usop].byname() # XXX µs/op hardcoded. FIXME disk/*
Bn = Bu[Unit("s/op")].convert_unit(u"µs/op").byname()
S = dict((_, Bn[_].stats()) for _ in Bn) # {} name -> stats
yticks = set()
......@@ -437,8 +440,10 @@ def plotnode1(ax, B, w=0.05, details=False):
for name in namev:
if _lat1_skipname(name):
continue
b = Bn[name]
s = b.stats()
s = S[name]
#if details:
# print '%s:\t%s' % (name, s)
node, xname = name.split('/', 1)
yticks.add(s.avg)
......@@ -589,9 +594,9 @@ def main():
ax2 = plt.subplot2grid((7,2), (0,1), rowspan=6)
ax2.set_title(u'latency, µs/object (↓ is better)')
Slat = xseriesof(Bu[Unit(u'latency-µs/object')])
Slat = xseriesof(Bu[Unit(u'latency-s/object')].convert_unit(u'latency-µs/object'))
if Slat is None:
raise RuntimeError('%s: latency-µs/object: series not found' % (labkey,))
raise RuntimeError('%s: latency-s/object: series not found' % (labkey,))
plotseries(ax2, labkey, Slat)
......@@ -630,6 +635,12 @@ def main():
ax211.set_xticks([])
ax211.set_xticklabels([])
plotnode1(ax211, B, w=0.007, details=True)
# small title for ax211
#ax211.text(0.5, 1.005, 'basic ops, µs', ha='center', fontsize='x-small', transform=ax211.transAxes)
bb = ax211.get_legend().get_bbox_to_anchor()
xc, ymax = ax211.transAxes.inverted().transform(((bb.xmin + bb.xmax) / 2., bb.ymax))
xc += 1.5 # FIXME because bb.xmax = xmin
ax211.text(xc, ymax + 0.005, 'basic ops, µs/op', ha='center', fontsize='x-small', transform=ax211.transAxes)
# mark_inset(ax21, ax211, ...)
rect = TransformedBbox(ax211.viewLim, ax21.transData)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment