| # Copyright 2017 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import json |
| import math |
| import numbers |
| import pprint |
| import random |
| |
| import six |
| from six.moves import range # pylint: disable=redefined-builtin |
| from tracing.proto import histogram_proto |
| from tracing.value.diagnostics import diagnostic |
| from tracing.value.diagnostics import diagnostic_ref |
| from tracing.value.diagnostics import generic_set |
| from tracing.value.diagnostics import reserved_infos |
| from tracing.value.diagnostics import unmergeable_diagnostic_set |
| |
| |
| try: |
| StringTypes = six.string_types # pylint: disable=invalid-name |
| except NameError: |
| StringTypes = str |
| |
| |
| # pylint: disable=too-many-lines |
| # TODO(#3613) Split this file. |
| |
| |
| # This should be equal to sys.float_info.max, but that value might differ |
| # between platforms, whereas ECMA Script specifies this value for all platforms. |
| # The specific value should not matter in normal practice. |
| JS_MAX_VALUE = 1.7976931348623157e+308 |
| DEFAULT_ITERATION_FOR_BOOTSTRAP_RESAMPLING = 500 |
| |
| |
| # Converts the given percent to a string in the following format: |
| # 0.x produces '0x0', |
| # 0.xx produces '0xx', |
| # 0.xxy produces '0xx_y', |
| # 1.0 produces '100'. |
| def PercentToString(percent, force3=False): |
| if percent < 0 or percent > 1: |
| raise ValueError('percent must be in [0,1]') |
| if percent == 0: |
| return '000' |
| if percent == 1: |
| return '100' |
| s = str(percent) |
| if s[1] != '.': |
| raise ValueError('Unexpected percent') |
| s += '0' * max(4 - len(s), 0) |
| if len(s) > 4: |
| if force3: |
| s = s[:4] |
| else: |
| s = s[:4] + '_' + s[4:] |
| return '0' + s[2:] |
| |
| |
| def PercentFromString(s): |
| return float(s[0] + '.' + s[1:].replace('_', '')) |
| |
| |
| # This variation of binary search returns the index |hi| into |ary| for which |
| # callback(ary[hi]) < 0 and callback(ary[hi-1]) >= 0 |
| # This function assumes that map(callback, ary) is sorted descending. |
| def FindHighIndexInSortedArray(ary, callback): |
| lo = 0 |
| hi = len(ary) |
| while lo < hi: |
| mid = (lo + hi) >> 1 |
| if callback(ary[mid]) >= 0: |
| lo = mid + 1 |
| else: |
| hi = mid |
| return hi |
| |
| |
| # Modifies |samples| in-place to reduce its length to |count|, discarding random |
| # elements. |
| def UniformlySampleArray(samples, count): |
| while len(samples) > count: |
| samples.pop(int(random.uniform(0, len(samples)))) |
| return samples |
| |
| |
| # When processing a stream of samples, call this method for each new sample in |
| # order to decide whether to keep it in |samples|. |
| # Modifies |samples| in-place such that its length never exceeds |num_samples|. |
| # After |stream_length| samples have been processed, each sample has equal |
| # probability of being retained in |samples|. |
| # The order of samples is not preserved after |stream_length| exceeds |
| # |num_samples|. |
| def UniformlySampleStream(samples, stream_length, new_element, num_samples): |
| if stream_length <= num_samples: |
| if len(samples) >= stream_length: |
| samples[stream_length - 1] = new_element |
| else: |
| samples.append(new_element) |
| return |
| |
| prob_keep = num_samples / stream_length |
| if random.random() > prob_keep: |
| # reject new_sample |
| return |
| |
| # replace a random element |
| samples[int(math.floor(random.random() * num_samples))] = new_element |
| |
| |
| # Merge two sets of samples that were assembled using UniformlySampleStream(). |
| # Modify |a_samples| in-place such that all of the samples in |a_samples| and |
| # |b_samples| have equal probability of being retained in |a_samples|. |
| def MergeSampledStreams(a_samples, a_stream_length, |
| b_samples, b_stream_length, num_samples): |
| if b_stream_length < num_samples: |
| for i in range(min(b_stream_length, len(b_samples))): |
| UniformlySampleStream( |
| a_samples, a_stream_length + i + 1, b_samples[i], num_samples) |
| return |
| |
| if a_stream_length < num_samples: |
| temp_samples = list(b_samples) |
| for i in range(min(a_stream_length, len(a_samples))): |
| UniformlySampleStream( |
| temp_samples, b_stream_length + i + 1, a_samples[i], num_samples) |
| for i, temp_sample in enumerate(temp_samples): |
| a_samples[i] = temp_sample |
| return |
| |
| prob_swap = b_stream_length / (a_stream_length + b_stream_length) |
| for i in range(min(num_samples, len(b_samples))): |
| if random.random() < prob_swap: |
| a_samples[i] = b_samples[i] |
| |
| |
| def Percentile(ary, percent): |
| if percent < 0 or percent > 1: |
| raise ValueError('percent must be in [0,1]') |
| ary = list(ary) |
| ary.sort() |
| return ary[int((len(ary) - 1) * percent)] |
| |
| |
| class HistogramError(ValueError): |
| """Base execption type for Histogram related exceptions.""" |
| |
| |
| class InvalidBucketError(HistogramError): |
| """Context-carrying exception type for invalid bucket values.""" |
| |
| def __init__(self, bucket_index, value, buckets): |
| """Raised when a provided index to a bucket is not valid. |
| |
| Arguments: |
| - bucket_index: a numeric index. |
| - value: a dict associated with the provided bucket index. |
| - buckets: a list of valid bucket definitions. |
| """ |
| super(InvalidBucketError, self).__init__() |
| self.bucket_index = bucket_index |
| self.value = value |
| self.buckets = buckets |
| |
| def __str__(self): |
| return 'Invalid Bucket: %s not a valid offset from [0..%s); value = %r' % ( |
| self.bucket_index, len(self.buckets), self.value) |
| |
| |
| class Range(object): |
| __slots__ = '_empty', '_min', '_max' |
| |
| def __init__(self): |
| self._empty = True |
| self._min = None |
| self._max = None |
| |
| def __eq__(self, other): |
| if not isinstance(other, Range): |
| return False |
| if self.empty and other.empty: |
| return True |
| if self.empty != other.empty: |
| return False |
| return (self.min == other.min) and (self.max == other.max) |
| |
| def __hash__(self): |
| return id(self) |
| |
| @staticmethod |
| def FromExplicitRange(lower, upper): |
| r = Range() |
| r._min = lower |
| r._max = upper |
| r._empty = False |
| return r |
| |
| @property |
| def empty(self): |
| return self._empty |
| |
| @property |
| def min(self): |
| return self._min |
| |
| @property |
| def max(self): |
| return self._max |
| |
| @property |
| def center(self): |
| return (self._min + self._max) * 0.5 |
| |
| @property |
| def duration(self): |
| if self.empty: |
| return 0 |
| return self._max - self._min |
| |
| def AddValue(self, x): |
| if self._empty: |
| self._empty = False |
| self._min = x |
| self._max = x |
| return |
| |
| self._max = max(x, self._max) |
| self._min = min(x, self._min) |
| |
| def AddRange(self, other): |
| if other.empty: |
| return |
| self.AddValue(other.min) |
| self.AddValue(other.max) |
| |
| |
| # This class computes statistics online in O(1). |
| class RunningStatistics(object): |
| __slots__ = ( |
| '_count', '_mean', '_max', '_min', '_sum', '_variance', '_meanlogs') |
| |
| def __init__(self): |
| self._count = 0 |
| self._mean = 0.0 |
| self._max = -JS_MAX_VALUE |
| self._min = JS_MAX_VALUE |
| self._sum = 0.0 |
| self._variance = 0.0 |
| # Mean of logarithms of samples, or None if any samples were <= 0. |
| self._meanlogs = 0.0 |
| |
| @property |
| def count(self): |
| return self._count |
| |
| @property |
| def geometric_mean(self): |
| if self._meanlogs is None: |
| return None |
| return math.exp(self._meanlogs) |
| |
| @property |
| def mean(self): |
| if self._count == 0: |
| return None |
| return self._mean |
| |
| @property |
| def max(self): |
| return self._max |
| |
| @property |
| def min(self): |
| return self._min |
| |
| @property |
| def sum(self): |
| return self._sum |
| |
| # This returns the variance of the samples after Bessel's correction has |
| # been applied. |
| @property |
| def variance(self): |
| if self.count == 0 or self._variance is None: |
| return None |
| if self.count == 1: |
| return 0 |
| return self._variance / (self.count - 1) |
| |
| # This returns the standard deviation of the samples after Bessel's |
| # correction has been applied. |
| @property |
| def stddev(self): |
| if self.count == 0 or self._variance is None: |
| return None |
| return math.sqrt(self.variance) |
| |
| def Add(self, x): |
| self._count += 1 |
| x = float(x) |
| self._max = max(self._max, x) |
| self._min = min(self._min, x) |
| self._sum += x |
| |
| if x <= 0.0: |
| self._meanlogs = None |
| elif self._meanlogs is not None: |
| self._meanlogs += (math.log(abs(x)) - self._meanlogs) / self.count |
| |
| # The following uses Welford's algorithm for computing running mean and |
| # variance. See http://www.johndcook.com/blog/standard_deviation. |
| if self.count == 1: |
| self._mean = x |
| self._variance = 0.0 |
| else: |
| old_mean = self._mean |
| old_variance = self._variance |
| |
| # Using the 2nd formula for updating the mean yields better precision but |
| # it doesn't work for the case oldMean is Infinity. Hence we handle that |
| # case separately. |
| if abs(old_mean) == float('inf'): |
| self._mean = self._sum / self.count |
| else: |
| self._mean = old_mean + float(x - old_mean) / self.count |
| self._variance = old_variance + (x - old_mean) * (x - self._mean) |
| |
| def Merge(self, other): |
| result = RunningStatistics() |
| result._count = self._count + other._count |
| result._sum = self._sum + other._sum |
| result._min = min(self._min, other._min) |
| result._max = max(self._max, other._max) |
| if result._count == 0: |
| result._mean = 0.0 |
| result._variance = 0.0 |
| result._meanlogs = 0.0 |
| else: |
| # Combine the mean and the variance using the formulas from |
| # https://goo.gl/ddcAep. |
| result._mean = float(result._sum) / result._count |
| delta_mean = (self._mean or 0.0) - (other._mean or 0.0) |
| result._variance = self._variance + other._variance + ( |
| self._count * other._count * delta_mean * delta_mean / result._count) |
| |
| # Merge the arithmetic means of logarithms of absolute values of samples, |
| # weighted by counts. |
| if self._meanlogs is None or other._meanlogs is None: |
| result._meanlogs = None |
| else: |
| result._meanlogs = (self._count * self._meanlogs + |
| other._count * other._meanlogs) / result._count |
| return result |
| |
| def AsDict(self): |
| if self._count == 0: |
| return [] |
| |
| # Javascript automatically converts between ints and floats. |
| # It's more efficient to serialize integers as ints than floats. |
| def FloatAsFloatOrInt(x): |
| if x is not None and x.is_integer(): |
| return int(x) |
| return x |
| |
| # It's more efficient to serialize these fields in an array. If you add any |
| # other fields, you should re-evaluate whether it would be more efficient to |
| # serialize as a dict. |
| return [ |
| self._count, |
| FloatAsFloatOrInt(self._max), |
| FloatAsFloatOrInt(self._meanlogs), |
| FloatAsFloatOrInt(self._mean), |
| FloatAsFloatOrInt(self._min), |
| FloatAsFloatOrInt(self._sum), |
| FloatAsFloatOrInt(self._variance), |
| ] |
| |
| def AsProto(self): |
| proto = histogram_proto.Pb2().RunningStatistics() |
| |
| if self._count == 0: |
| return proto |
| |
| # We can't currently set None; setting 0 at least means nothing is sent |
| # on the wire. |
| proto.count = self._count or 0 |
| proto.max = self._max or 0 |
| proto.meanlogs = self._meanlogs or 0 |
| proto.mean = self._mean or 0 |
| proto.min = self._min or 0 |
| proto.sum = self._sum or 0 |
| proto.variance = self._variance or 0 |
| |
| return proto |
| |
| @staticmethod |
| def FromDict(dct): |
| result = RunningStatistics() |
| if len(dct) != 7: |
| return result |
| |
| def AsFloatOrNone(x): |
| if x is None: |
| return x |
| return float(x) |
| [result._count, result._max, result._meanlogs, result._mean, result._min, |
| result._sum, result._variance] = [int(dct[0])] + [ |
| AsFloatOrNone(x) for x in dct[1:]] |
| return result |
| |
| @staticmethod |
| def FromProto(proto): |
| result = RunningStatistics() |
| |
| # TODO(http://crbug.com/1029452): Make these IntValue in the proto, since |
| # we apparently need to be able to tell None from 0. |
| result._count = proto.count |
| result._max = proto.max |
| result._meanlogs = proto.meanlogs |
| result._mean = proto.mean |
| result._min = proto.min |
| result._sum = proto.sum |
| result._variance = proto.variance |
| |
| return result |
| |
| |
| class DiagnosticMap(dict): |
| __slots__ = ('_allow_reserved_names',) |
| |
| def __init__(self, *args, **kwargs): |
| self._allow_reserved_names = True |
| dict.__init__(self, *args, **kwargs) |
| |
| def DisallowReservedNames(self): |
| self._allow_reserved_names = False |
| |
| def __setitem__(self, name, diag): |
| if not isinstance(name, StringTypes): |
| raise TypeError('name must be string') |
| if not isinstance(diag, (diagnostic.Diagnostic, |
| diagnostic_ref.DiagnosticRef)): |
| raise TypeError('diag must be Diagnostic or DiagnosticRef') |
| if (not self._allow_reserved_names and |
| not isinstance(diag, |
| unmergeable_diagnostic_set.UnmergeableDiagnosticSet) and |
| not isinstance(diag, diagnostic_ref.DiagnosticRef)): |
| expected_type = reserved_infos.GetTypeForName(name) |
| if expected_type and diag.__class__.__name__ != expected_type: |
| raise TypeError('Diagnostics names "%s" must be %s, not %s' % |
| (name, expected_type, diag.__class__.__name__)) |
| dict.__setitem__(self, name, diag) |
| |
| @staticmethod |
| def Deserialize(data, deserializer): |
| dm = DiagnosticMap() |
| dm.DeserializeAdd(data, deserializer) |
| return dm |
| |
| def DeserializeAdd(self, data, deserializer): |
| for i in data: |
| self.update(deserializer.GetDiagnostic(i)) |
| |
| @staticmethod |
| def FromDict(dct): |
| dm = DiagnosticMap() |
| dm.AddDicts(dct) |
| return dm |
| |
| @staticmethod |
| def FromProto(proto): |
| dm = DiagnosticMap() |
| dm.AddProtos(proto.diagnostic_map) |
| return dm |
| |
| def AddDicts(self, dct): |
| for name, diagnostic_dict in dct.items(): |
| if name == 'tagmap': |
| continue |
| if isinstance(diagnostic_dict, StringTypes): |
| self[name] = diagnostic_ref.DiagnosticRef(diagnostic_dict) |
| elif diagnostic_dict['type'] not in [ |
| 'RelatedHistogramMap', 'RelatedHistogramBreakdown', 'TagMap']: |
| # Ignore RelatedHistograms and TagMaps. |
| # TODO(benjhayden): Forget about them in 2019 Q2. |
| self[name] = diagnostic.Diagnostic.FromDict(diagnostic_dict) |
| |
| def AddProtos(self, protos): |
| for name, diagnostic_proto in protos.items(): |
| if diagnostic_proto.HasField('shared_diagnostic_guid'): |
| self[name] = diagnostic_ref.DiagnosticRef( |
| diagnostic_proto.shared_diagnostic_guid) |
| else: |
| self[name] = diagnostic.Diagnostic.FromProto(diagnostic_proto) |
| |
| def ResolveSharedDiagnostics(self, histograms, required=False): |
| for name, diag in self.items(): |
| if not isinstance(diag, diagnostic_ref.DiagnosticRef): |
| continue |
| guid = diag.guid |
| diag = histograms.LookupDiagnostic(guid) |
| if isinstance(diag, diagnostic.Diagnostic): |
| self[name] = diag |
| elif required: |
| raise ValueError('Unable to find shared Diagnostic ' + guid) |
| |
| def Serialize(self, serializer): |
| return [serializer.GetOrAllocateDiagnosticId(name, diag) |
| for name, diag in self.items()] |
| |
| def AsDict(self): |
| dct = {} |
| for name, diag in self.items(): |
| dct[name] = diag.AsDictOrReference() |
| return dct |
| |
| def AsProto(self, proto): |
| for name, diag in self.items(): |
| proto.diagnostic_map[name].CopyFrom(diag.AsProtoOrReference()) |
| return proto |
| |
| def Merge(self, other): |
| for name, other_diagnostic in other.items(): |
| if name not in self: |
| self[name] = other_diagnostic |
| continue |
| my_diagnostic = self[name] |
| if my_diagnostic.CanAddDiagnostic(other_diagnostic): |
| my_diagnostic.AddDiagnostic(other_diagnostic) |
| continue |
| self[name] = unmergeable_diagnostic_set.UnmergeableDiagnosticSet([ |
| my_diagnostic, other_diagnostic]) |
| |
| |
| MAX_DIAGNOSTIC_MAPS = 16 |
| |
| |
| class HistogramBin(object): |
| __slots__ = '_range', '_count', '_diagnostic_maps' |
| |
| def __init__(self, rang): |
| self._range = rang |
| self._count = 0 |
| self._diagnostic_maps = [] |
| |
| def AddSample(self, unused_x): |
| self._count += 1 |
| |
| @property |
| def count(self): |
| return self._count |
| |
| @property |
| def range(self): |
| return self._range |
| |
| def AddBin(self, other): |
| self._count += other.count |
| |
| @property |
| def diagnostic_maps(self): |
| return self._diagnostic_maps |
| |
| def AddDiagnosticMap(self, diagnostics): |
| UniformlySampleStream( |
| self._diagnostic_maps, self.count, diagnostics, MAX_DIAGNOSTIC_MAPS) |
| |
| def FromDict(self, dct): |
| self._count = dct[0] |
| if len(dct) > 1: |
| for diagnostic_map_dict in dct[1]: |
| self._diagnostic_maps.append(DiagnosticMap.FromDict( |
| diagnostic_map_dict)) |
| |
| def FromProto(self, proto): |
| self._count = proto.bin_count |
| |
| for diagnostic_map in proto.diagnostic_maps: |
| self._diagnostic_maps.append(DiagnosticMap.FromProto(diagnostic_map)) |
| |
| def AsDict(self): |
| if len(self._diagnostic_maps) == 0: |
| return [self.count] |
| return [self.count, [d.AsDict() for d in self._diagnostic_maps]] |
| |
| def AsProto(self): |
| proto = histogram_proto.Pb2().Bin() |
| proto.bin_count = self._count |
| if len(self._diagnostic_maps) == 0: |
| return proto |
| |
| proto.diagnostic_maps.extend([d.AsProto() for d in self._diagnostic_maps]) |
| return proto |
| |
| def Serialize(self, serializer): |
| if len(self._diagnostic_maps) == 0: |
| return self.count |
| return [self.count] + [ |
| [None] + d.Serialize(serializer) for d in self._diagnostic_maps] |
| |
| def Deserialize(self, data, deserializer): |
| if not isinstance(data, list): |
| self._count = data |
| return |
| self._count = data[0] |
| for sample in data[1:]: |
| # TODO(benjhayden): class Sample |
| if not isinstance(sample, list): |
| continue |
| self._diagnostic_maps.append(DiagnosticMap.Deserialize( |
| sample[1:], deserializer)) |
| |
| |
| # This list should be kept in sync with tracing/tracing/base/unit.html |
| # and tracing/tracing/value/histogram.cc. |
| # TODO(#3814) Presubmit to compare with unit.html. |
| UNIT_NAMES = [ |
| 'ms', |
| 'msBestFitFormat', |
| 'tsMs', |
| 'n%', |
| 'sizeInBytes', |
| 'bytesPerSecond', |
| 'J', # Joule |
| 'W', # Watt |
| 'A', # Ampere |
| 'Ah', # Ampere-hours |
| 'V', # Volt |
| 'Hz', # Hertz |
| 'unitless', |
| 'count', |
| 'sigma', |
| ] |
| |
| def ExtendUnitNames(): |
| # Use a function in order to avoid cluttering the global namespace with a loop |
| # variable. |
| for name in list(UNIT_NAMES): |
| UNIT_NAMES.append(name + '_biggerIsBetter') |
| UNIT_NAMES.append(name + '_smallerIsBetter') |
| UNIT_NAMES.append(name + '-') |
| UNIT_NAMES.append(name + '+') |
| |
| ExtendUnitNames() |
| |
| def UnrecognizedUnitMessage(unit): |
| output = 'Unrecognized unit "%r". ' % unit |
| output += "Use one of the following:\n" |
| output += pprint.pformat(UNIT_NAMES) |
| return output |
| |
| |
| class Scalar(object): |
| __slots__ = '_unit', '_value' |
| |
| def __init__(self, unit, value): |
| assert unit in UNIT_NAMES, UnrecognizedUnitMessage(unit) |
| self._unit = unit |
| self._value = value |
| |
| @property |
| def unit(self): |
| return self._unit |
| |
| @property |
| def value(self): |
| return self._value |
| |
| def AsDict(self): |
| return {'type': 'scalar', 'unit': self.unit, 'value': self.value} |
| |
| @staticmethod |
| def FromDict(dct): |
| return Scalar(dct['unit'], dct['value']) |
| |
| |
| DEFAULT_SUMMARY_OPTIONS = { |
| 'avg': True, |
| 'geometricMean': False, |
| 'std': True, |
| 'count': True, |
| 'sum': True, |
| 'min': True, |
| 'max': True, |
| 'nans': False, |
| # Don't include 'percentile' here. Its default value is [], which is |
| # modifiable. Callers may push to it, so there must be a different Array |
| # instance for each Histogram instance. |
| } |
| |
| |
| class Histogram(object): |
| __slots__ = ( |
| '_bin_boundaries_dict', |
| '_description', |
| '_name', |
| '_diagnostics', |
| '_nan_diagnostic_maps', |
| '_num_nans', |
| '_running', |
| '_sample_values', |
| '_sample_means', |
| '_summary_options', |
| '_unit', |
| '_bins', |
| '_max_num_sample_values') |
| |
| def __init__(self, name, unit, bin_boundaries=None): |
| assert unit in UNIT_NAMES, UnrecognizedUnitMessage(unit) |
| |
| if bin_boundaries is None: |
| base_unit = unit.split('_')[0].strip('+-') |
| bin_boundaries = DEFAULT_BOUNDARIES_FOR_UNIT[base_unit] |
| |
| # Serialize bin boundaries here instead of holding a reference to it in case |
| # it is modified. |
| self._bin_boundaries_dict = bin_boundaries.AsDict() |
| |
| # HistogramBinBoundaries creates empty HistogramBins. Save memory by sharing |
| # those empty HistogramBin instances with other Histograms. Wait to copy |
| # HistogramBins until we need to modify it (copy-on-write). |
| self._bins = list(bin_boundaries.bins) |
| self._description = '' |
| self._name = name |
| self._diagnostics = DiagnosticMap() |
| self._diagnostics.DisallowReservedNames() |
| self._nan_diagnostic_maps = [] |
| self._num_nans = 0 |
| self._running = None |
| self._sample_values = [] |
| self._sample_means = [] |
| self._summary_options = dict(DEFAULT_SUMMARY_OPTIONS) |
| self._summary_options['percentile'] = [] |
| self._summary_options['ci'] = [] |
| self._unit = unit |
| |
| self._max_num_sample_values = self._GetDefaultMaxNumSampleValues() |
| |
| @classmethod |
| def Create(cls, name, unit, samples, bin_boundaries=None, description='', |
| summary_options=None, diagnostics=None): |
| hist = cls(name, unit, bin_boundaries) |
| hist.description = description |
| if summary_options: |
| hist.CustomizeSummaryOptions(summary_options) |
| if diagnostics: |
| for diag_name, diag in diagnostics.items(): |
| hist.diagnostics[diag_name] = diag |
| |
| if not isinstance(samples, list): |
| samples = [samples] |
| for sample in samples: |
| if isinstance(sample, tuple) and len(sample) == 2: |
| hist.AddSample(sample[0], sample[1]) |
| else: |
| hist.AddSample(sample) |
| |
| return hist |
| |
| @property |
| def description(self): |
| return self._description |
| |
| @description.setter |
| def description(self, d): |
| self._description = d |
| |
| @property |
| def nan_diagnostic_maps(self): |
| return self._nan_diagnostic_maps |
| |
| @property |
| def unit(self): |
| return self._unit |
| |
| @property |
| def running(self): |
| return self._running |
| |
| @property |
| def max_num_sample_values(self): |
| return self._max_num_sample_values |
| |
| @max_num_sample_values.setter |
| def max_num_sample_values(self, n): |
| self._max_num_sample_values = n |
| UniformlySampleArray(self._sample_values, self._max_num_sample_values) |
| |
| @property |
| def sample_values(self): |
| return self._sample_values |
| |
| @property |
| def name(self): |
| return self._name |
| |
| @property |
| def bins(self): |
| return self._bins |
| |
| @property |
| def diagnostics(self): |
| return self._diagnostics |
| |
| def _DeserializeStatistics(self): |
| statistics_names = self.diagnostics.get( |
| reserved_infos.STATISTICS_NAMES.name) |
| if not statistics_names: |
| return |
| for stat_name in statistics_names: |
| if stat_name.startswith('pct_'): |
| percent = PercentFromString(stat_name[4:]) |
| self._summary_options.get('percentile').append(percent) |
| elif stat_name.startswith('ipr_'): |
| lower = PercentFromString(stat_name[4:7]) |
| upper = PercentFromString(stat_name[8:]) |
| self._summary_options.get('iprs').push( |
| Range.FromExplicitRange(lower, upper)) |
| for stat_name in self._summary_options: |
| if stat_name in ['percentile', 'iprs']: |
| continue |
| self._summary_options[stat_name] = stat_name in statistics_names |
| |
| def _DeserializeBin(self, i, bin_data, deserializer): |
| # Copy HistogramBin on write, share the rest with the other |
| # Histograms that use the same HistogramBinBoundaries. |
| self._bins[i] = HistogramBin(self._bins[i].range) |
| self._bins[i].Deserialize(bin_data, deserializer) |
| |
| # TODO(benjhayden): Remove after class Sample. |
| if not isinstance(bin_data, list): |
| return |
| for sample in bin_data[1:]: |
| if isinstance(sample, list): |
| sample = sample[0] |
| self._sample_values.append(sample) |
| |
| |
| def _DeserializeBins(self, bins, deserializer): |
| if isinstance(bins, list): |
| for i, bin_data in enumerate(bins): |
| self._DeserializeBin(i, bin_data, deserializer) |
| else: |
| for i, bin_data in bins.items(): |
| self._DeserializeBin(int(i), bin_data, deserializer) |
| |
| @staticmethod |
| def Deserialize(data, deserializer): |
| name, unit, boundaries, diagnostics, running, bins, nan_bin = data |
| name = deserializer.GetObject(name) |
| boundaries = HistogramBinBoundaries.FromDict( |
| deserializer.GetObject(boundaries)) |
| hist = Histogram(name, unit, boundaries) |
| |
| hist._diagnostics.DeserializeAdd(diagnostics, deserializer) |
| |
| desc = hist.diagnostics.get(reserved_infos.DESCRIPTION.name) |
| if desc: |
| hist.description = list(desc)[0] |
| |
| hist._DeserializeStatistics() |
| |
| if running: |
| hist._running = RunningStatistics.FromDict(running) |
| |
| if bins: |
| hist._DeserializeBins(bins, deserializer) |
| |
| if isinstance(nan_bin, list): |
| # TODO(benjhayden): hist._nan_bin |
| hist._num_nans = nan_bin[0] |
| for sample in nan_bin[1:]: |
| # TODO(benjhayden): class Sample |
| hist._nan_diagnostic_maps.append( |
| DiagnosticMap.Deserialize(sample[1:], deserializer)) |
| elif nan_bin: |
| hist._num_nans = nan_bin |
| |
| return hist |
| |
| @staticmethod |
| def FromDict(dct): |
| boundaries = HistogramBinBoundaries.FromDict(dct.get('binBoundaries')) |
| hist = Histogram(dct['name'], dct['unit'], boundaries) |
| if 'description' in dct: |
| hist._description = dct['description'] |
| if 'diagnostics' in dct: |
| hist._diagnostics.AddDicts(dct['diagnostics']) |
| if 'allBins' in dct: |
| if isinstance(dct['allBins'], list): |
| for i, bin_dct in enumerate(dct['allBins']): |
| # Copy HistogramBin on write, share the rest with the other |
| # Histograms that use the same HistogramBinBoundaries. |
| hist._bins[i] = HistogramBin(hist._bins[i].range) |
| hist._bins[i].FromDict(bin_dct) |
| else: |
| for i, bin_dct in dct['allBins'].items(): |
| i = int(i) |
| # Check whether i is a valid index before using it as a list index. |
| if i >= len(hist._bins) or i < 0: |
| raise InvalidBucketError(i, bin_dct, hist._bins) |
| hist._bins[i] = HistogramBin(hist._bins[i].range) |
| hist._bins[i].FromDict(bin_dct) |
| if 'running' in dct: |
| hist._running = RunningStatistics.FromDict(dct['running']) |
| if 'summaryOptions' in dct: |
| if 'iprs' in dct['summaryOptions']: |
| dct['summaryOptions']['iprs'] = [ |
| Range.FromExplicitRange(r[0], r[1]) |
| for r in dct['summaryOptions']['iprs']] |
| hist.CustomizeSummaryOptions(dct['summaryOptions']) |
| if 'maxNumSampleValues' in dct: |
| hist._max_num_sample_values = dct['maxNumSampleValues'] |
| if 'sampleValues' in dct: |
| hist._sample_values = dct['sampleValues'] |
| if 'numNans' in dct: |
| hist._num_nans = dct['numNans'] |
| if 'nanDiagnostics' in dct: |
| for map_dct in dct['nanDiagnostics']: |
| hist._nan_diagnostic_maps.append(DiagnosticMap.FromDict(map_dct)) |
| return hist |
| |
| @staticmethod |
| def FromProto(proto): |
| if not proto.HasField('unit'): |
| raise ValueError('The "unit" field is required.') |
| if not proto.name: |
| raise ValueError('The "name" field is required.') |
| |
| boundaries = HistogramBinBoundaries.FromProto(proto.bin_boundaries) |
| unit = histogram_proto.UnitFromProto(proto.unit) |
| |
| hist = Histogram(proto.name, unit, boundaries) |
| |
| if proto.description: |
| hist._description = proto.description |
| if proto.HasField('diagnostics'): |
| hist._diagnostics.AddProtos(proto.diagnostics.diagnostic_map) |
| for i, bin_spec in proto.all_bins.items(): |
| i = int(i) |
| # Check whether i is a valid index before using it as a list index. |
| if i >= len(hist._bins) or i < 0: |
| raise InvalidBucketError(i, str(bin_spec), hist._bins) |
| hist._bins[i] = HistogramBin(hist._bins[i].range) |
| hist._bins[i].FromProto(bin_spec) |
| if proto.HasField('running'): |
| hist._running = RunningStatistics.FromProto(proto.running) |
| if proto.HasField('summary_options'): |
| options_dict = { |
| 'avg': proto.summary_options.avg, |
| 'geometricMean': proto.summary_options.geometric_mean, |
| 'std': proto.summary_options.std, |
| 'count': proto.summary_options.count, |
| 'sum': proto.summary_options.sum, |
| 'min': proto.summary_options.min, |
| 'max': proto.summary_options.max, |
| 'nans': proto.summary_options.nans, |
| 'percentile': proto.summary_options.percentile, |
| } |
| hist.CustomizeSummaryOptions(options_dict) |
| if proto.max_num_sample_values: |
| hist._max_num_sample_values = proto.max_num_sample_values |
| if proto.sample_values: |
| hist._sample_values = proto.sample_values |
| if proto.num_nans: |
| hist._num_nans = proto.num_nans |
| |
| # TODO: NaN diagnostics. |
| |
| return hist |
| |
| @property |
| def num_values(self): |
| if self._running is None: |
| return 0 |
| return self._running.count |
| |
| @property |
| def num_nans(self): |
| return self._num_nans |
| |
| @property |
| def average(self): |
| if self._running is None: |
| return None |
| return self._running.mean |
| |
| @property |
| def standard_deviation(self): |
| if self._running is None: |
| return None |
| return self._running.stddev |
| |
| @property |
| def geometric_mean(self): |
| if self._running is None: |
| return 0 |
| return self._running.geometric_mean |
| |
| @property |
| def sum(self): |
| if self._running is None: |
| return 0 |
| return self._running.sum |
| |
| def GetApproximatePercentile(self, percent): |
| if percent < 0 or percent > 1: |
| raise ValueError('percent must be in [0,1]') |
| if self.num_values == 0: |
| return 0 |
| |
| if len(self._bins) == 1: |
| sorted_sample_values = list(self._sample_values) |
| sorted_sample_values.sort() |
| return sorted_sample_values[ |
| int((len(sorted_sample_values) - 1) * percent)] |
| |
| values_to_skip = math.floor((self.num_values - 1) * percent) |
| for hbin in self._bins: |
| values_to_skip -= hbin.count |
| if values_to_skip >= 0: |
| continue |
| if hbin.range.min == -JS_MAX_VALUE: |
| return hbin.range.max |
| if hbin.range.max == JS_MAX_VALUE: |
| return hbin.range.min |
| return hbin.range.center |
| return self._bins[len(self._bins) - 1].range.min |
| |
| def _ResampleMean(self, percent): |
| if percent <= 0 or percent >= 1: |
| raise ValueError('percent must be in (0,1)') |
| filtered_samples = [ |
| x for x in self._sample_values if isinstance(x, (float, int)) |
| ] |
| if not filtered_samples: |
| return [0, 0] |
| |
| sample_count = len(filtered_samples) |
| iterations = DEFAULT_ITERATION_FOR_BOOTSTRAP_RESAMPLING |
| if sample_count == 1: |
| return [filtered_samples[0], filtered_samples[0]] |
| if len(self._sample_means) != iterations: |
| self._sample_means = [] |
| for _ in range(0, iterations): |
| temp_sum = 0 |
| for _ in range(0, sample_count): |
| temp_sum += random.choice(filtered_samples) |
| self._sample_means.append(temp_sum / sample_count) |
| self._sample_means.sort() |
| |
| return [ |
| self._sample_means[int( |
| math.floor((iterations - 1) * (0.5 - percent / 2)))], |
| self._sample_means[int( |
| math.ceil((iterations - 1) * (0.5 + percent / 2)))], |
| ] |
| |
| def GetBinIndexForValue(self, value): |
| index = FindHighIndexInSortedArray( |
| self._bins, lambda b: (-1 if (value < b.range.max) else 1)) |
| if 0 <= index < len(self._bins): |
| return index |
| return len(self._bins) - 1 |
| |
| def GetBinForValue(self, value): |
| return self._bins[self.GetBinIndexForValue(value)] |
| |
| def AddSample(self, value, diagnostic_map=None): |
| if (diagnostic_map is not None and |
| not isinstance(diagnostic_map, DiagnosticMap)): |
| diagnostic_map = DiagnosticMap(diagnostic_map) |
| |
| if not isinstance(value, numbers.Number) or math.isnan(value): |
| self._num_nans += 1 |
| if diagnostic_map: |
| UniformlySampleStream(self._nan_diagnostic_maps, self.num_nans, |
| diagnostic_map, MAX_DIAGNOSTIC_MAPS) |
| else: |
| if self._running is None: |
| self._running = RunningStatistics() |
| self._running.Add(value) |
| |
| bin_index = self.GetBinIndexForValue(value) |
| hbin = self._bins[bin_index] |
| if hbin.count == 0: |
| hbin = HistogramBin(hbin.range) |
| self._bins[bin_index] = hbin |
| hbin.AddSample(value) |
| if diagnostic_map: |
| hbin.AddDiagnosticMap(diagnostic_map) |
| |
| UniformlySampleStream(self._sample_values, self.num_values + self.num_nans, |
| value, self.max_num_sample_values) |
| |
| def CanAddHistogram(self, other): |
| if self.unit != other.unit: |
| return False |
| return self._bin_boundaries_dict == other._bin_boundaries_dict |
| |
| def AddHistogram(self, other): |
| if not self.CanAddHistogram(other): |
| raise ValueError('Merging incompatible Histograms') |
| |
| MergeSampledStreams( |
| self.sample_values, self.num_values, |
| other.sample_values, other.num_values, |
| (self.max_num_sample_values + other.max_num_sample_values) / 2) |
| self._num_nans += other._num_nans |
| |
| if other.running is not None: |
| if self.running is None: |
| self._running = RunningStatistics() |
| self._running = self._running.Merge(other.running) |
| |
| for i, hbin in enumerate(other.bins): |
| mybin = self._bins[i] |
| if mybin.count == 0: |
| self._bins[i] = mybin = HistogramBin(mybin.range) |
| mybin.AddBin(hbin) |
| |
| self.diagnostics.Merge(other.diagnostics) |
| |
| def CustomizeSummaryOptions(self, options): |
| for key, value in options.items(): |
| self._summary_options[key] = value |
| |
| def Clone(self): |
| return Histogram.FromDict(self.AsDict()) |
| |
| def CloneEmpty(self): |
| return Histogram(self.name, self.unit, HistogramBinBoundaries.FromDict( |
| self._bin_boundaries_dict)) |
| |
| @property |
| def statistics_names(self): |
| names = set() |
| for stat_name, option in self._summary_options.items(): |
| if stat_name == 'percentile': |
| for pctile in option: |
| names.add('pct_' + PercentToString(pctile)) |
| elif stat_name == 'iprs': |
| for rang in option: |
| names.add('ipr_' + PercentToString(rang.min, True) + |
| '_' + PercentToString(rang.max, True)) |
| elif stat_name == 'ci': |
| for ci in option: |
| ps = PercentToString(ci) |
| names.add('ci_' + ps + '_lower') |
| names.add('ci_' + ps + '_upper') |
| names.add('ci_' + ps) |
| elif option: |
| names.add(stat_name) |
| return names |
| |
| def GetStatisticScalar(self, stat_name): |
| if stat_name == 'avg': |
| if self.average is None: |
| return None |
| return Scalar(self.unit, self.average) |
| |
| if stat_name == 'std': |
| if self.standard_deviation is None: |
| return None |
| return Scalar(self.unit, self.standard_deviation) |
| |
| if stat_name == 'geometricMean': |
| if self.geometric_mean is None: |
| return None |
| return Scalar(self.unit, self.geometric_mean) |
| |
| if stat_name in ['min', 'max', 'sum']: |
| if self._running is None: |
| self._running = RunningStatistics() |
| return Scalar(self.unit, getattr(self._running, stat_name)) |
| |
| if stat_name == 'nans': |
| return Scalar('count_smallerIsBetter', self.num_nans) |
| |
| if stat_name == 'count': |
| return Scalar('count_smallerIsBetter', self.num_values) |
| |
| if stat_name.startswith('pct_'): |
| if self.num_values == 0: |
| return None |
| percent = PercentFromString(stat_name[4:]) |
| percentile = self.GetApproximatePercentile(percent) |
| return Scalar(self.unit, percentile) |
| |
| if stat_name.startswith('ci_'): |
| if self.num_values == 0: |
| return None |
| ci = PercentFromString(stat_name[3:6]) |
| [ci_lower, ci_upper] = self._ResampleMean(ci) |
| if stat_name.endswith('lower'): |
| return Scalar(self.unit, ci_lower) |
| if stat_name.endswith('upper'): |
| return Scalar(self.unit, ci_upper) |
| return Scalar(self.unit, ci_upper - ci_lower) |
| |
| if stat_name.startswith('ipr_'): |
| lower = PercentFromString(stat_name[4:7]) |
| upper = PercentFromString(stat_name[8:]) |
| if lower >= upper: |
| raise ValueError('Invalid inter-percentile range: ' + stat_name) |
| return Scalar(self.unit, (self.GetApproximatePercentile(upper) - |
| self.GetApproximatePercentile(lower))) |
| |
| return None |
| |
| @property |
| def statistics_scalars(self): |
| results = {} |
| for name in self.statistics_names: |
| scalar = self.GetStatisticScalar(name) |
| if scalar: |
| results[name] = scalar |
| return results |
| |
| def Serialize(self, serializer): |
| nan_bin = self.num_nans |
| if self.nan_diagnostic_maps: |
| nan_bin = [nan_bin] + [ |
| [None] + dm.Serialize(serializer) for dm in self.nan_diagnostic_maps] |
| self.diagnostics[reserved_infos.STATISTICS_NAMES.name] = \ |
| generic_set.GenericSet(sorted(self.statistics_names)) |
| self.diagnostics[reserved_infos.DESCRIPTION.name] = \ |
| generic_set.GenericSet([self._description]) |
| return [ |
| serializer.GetOrAllocateId(self.name), |
| self.unit.replace( |
| '_biggerIsBetter', '+').replace('_smallerIsBetter', '-'), |
| serializer.GetOrAllocateId(self._bin_boundaries_dict), |
| self.diagnostics.Serialize(serializer), |
| self._running.AsDict() if self._running else 0, |
| self._SerializeBins(serializer), |
| nan_bin, |
| ] |
| |
| def AsDict(self): |
| dct = {'name': self.name, 'unit': self.unit} |
| if self._bin_boundaries_dict is not None: |
| dct['binBoundaries'] = self._bin_boundaries_dict |
| if self._description: |
| dct['description'] = self._description |
| if len(self.diagnostics): |
| dct['diagnostics'] = self.diagnostics.AsDict() |
| if self.max_num_sample_values != self._GetDefaultMaxNumSampleValues(): |
| dct['maxNumSampleValues'] = self.max_num_sample_values |
| if self.num_nans: |
| dct['numNans'] = self.num_nans |
| if self.nan_diagnostic_maps: |
| dct['nanDiagnostics'] = [m.AsDict() for m in self.nan_diagnostic_maps] |
| if self.num_values: |
| dct['sampleValues'] = list(self.sample_values) |
| dct['running'] = self._running.AsDict() |
| dct['allBins'] = self._GetAllBinsAsDict() |
| if dct['allBins'] is None: |
| del dct['allBins'] |
| |
| summary_options = {} |
| any_overridden_summary_options = False |
| for name, option in self._summary_options.items(): |
| if name in ('percentile', 'ci'): |
| if len(option) == 0: |
| continue |
| elif name == 'iprs': |
| if len(option) == 0: |
| continue |
| option = [[r.min, r.max] for r in option] |
| elif name == 'ci': |
| if len(option) == 0: |
| continue |
| elif option == DEFAULT_SUMMARY_OPTIONS[name]: |
| continue |
| summary_options[name] = option |
| any_overridden_summary_options = True |
| if any_overridden_summary_options: |
| dct['summaryOptions'] = summary_options |
| return dct |
| |
| def AsProto(self): |
| proto = histogram_proto.Pb2().Histogram() |
| |
| proto.name = self.name |
| unit = histogram_proto.ProtoFromUnit(self.unit) |
| proto.unit.unit = unit.unit |
| proto.unit.improvement_direction = unit.improvement_direction |
| |
| if self._bin_boundaries_dict: |
| bounds = HistogramBinBoundaries.ToProto(self._bin_boundaries_dict) |
| proto.bin_boundaries.first_bin_boundary = bounds.first_bin_boundary |
| proto.bin_boundaries.bin_specs.extend(bounds.bin_specs) |
| if self._description: |
| proto.description = self._description |
| if len(self.diagnostics): |
| self.diagnostics.AsProto(proto.diagnostics) |
| |
| if self.max_num_sample_values != self._GetDefaultMaxNumSampleValues(): |
| proto.max_num_sample_values = self.max_num_sample_values |
| if self.num_nans: |
| proto.num_nans = self.num_nans |
| if self.num_values: |
| proto.sample_values.extend(list(self.sample_values)) |
| proto.running.CopyFrom(self._running.AsProto()) |
| self._GetAllBinsAsProto(proto.all_bins) |
| |
| any_overridden_summary_options = any( |
| self._summary_options[k] != DEFAULT_SUMMARY_OPTIONS[k] |
| for k in DEFAULT_SUMMARY_OPTIONS) |
| |
| if any_overridden_summary_options or self._summary_options['percentile']: |
| # Note: iprs and ci are not supported in the proto format yet. |
| # Also, we must set all options or none (could be fixed by moving to bool |
| # wrappers in the proto). |
| proto.summary_options.avg = self._summary_options['avg'] |
| proto.summary_options.geometric_mean = ( |
| self._summary_options['geometricMean']) |
| proto.summary_options.std = self._summary_options['std'] |
| proto.summary_options.count = self._summary_options['count'] |
| proto.summary_options.sum = self._summary_options['sum'] |
| proto.summary_options.min = self._summary_options['min'] |
| proto.summary_options.max = self._summary_options['max'] |
| proto.summary_options.nans = self._summary_options['nans'] |
| proto.summary_options.percentile.extend( |
| self._summary_options['percentile']) |
| |
| return proto |
| |
| def _SerializeBins(self, serializer): |
| num_bins = len(self._bins) |
| empty_bins = 0 |
| for hbin in self._bins: |
| if hbin.count == 0: |
| empty_bins += 1 |
| if empty_bins == num_bins: |
| return None |
| |
| if empty_bins > (num_bins / 2): |
| all_bins_dict = {} |
| for i, hbin in enumerate(self._bins): |
| if hbin.count > 0: |
| all_bins_dict[i] = hbin.Serialize(serializer) |
| return all_bins_dict |
| |
| all_bins_list = [] |
| for hbin in self._bins: |
| all_bins_list.append(hbin.Serialize(serializer)) |
| return all_bins_list |
| |
| def _GetAllBinsAsDict(self): |
| num_bins = len(self._bins) |
| empty_bins = 0 |
| for hbin in self._bins: |
| if hbin.count == 0: |
| empty_bins += 1 |
| if empty_bins == num_bins: |
| return None |
| |
| if empty_bins > (num_bins / 2): |
| all_bins_dict = {} |
| for i, hbin in enumerate(self._bins): |
| if hbin.count > 0: |
| all_bins_dict[i] = hbin.AsDict() |
| return all_bins_dict |
| |
| all_bins_list = [] |
| for hbin in self._bins: |
| all_bins_list.append(hbin.AsDict()) |
| return all_bins_list |
| |
| def _GetAllBinsAsProto(self, proto_bin_map): |
| num_bins = len(self._bins) |
| empty_bins = 0 |
| for hbin in self._bins: |
| if hbin.count == 0: |
| empty_bins += 1 |
| if empty_bins == num_bins: |
| return |
| |
| for i, hbin in enumerate(self._bins): |
| if hbin.count > 0: |
| proto_bin_map[i].CopyFrom(hbin.AsProto()) |
| |
| def _GetDefaultMaxNumSampleValues(self): |
| return len(self._bins) * 10 |
| |
| |
| class HistogramBinBoundaries(object): |
| __slots__ = '_builder', '_range', '_bin_ranges', '_bins' |
| |
| CACHE = {} |
| SLICE_TYPE_LINEAR = 0 |
| SLICE_TYPE_EXPONENTIAL = 1 |
| |
| def __init__(self, min_bin_boundary): |
| self._builder = [min_bin_boundary] |
| self._range = Range() |
| self._range.AddValue(min_bin_boundary) |
| self._bin_ranges = None |
| self._bins = None |
| |
| @property |
| def range(self): |
| return self._range |
| |
| @staticmethod |
| def FromDict(dct): |
| if dct is None: |
| return HistogramBinBoundaries.SINGULAR |
| |
| cache_key = json.dumps(dct) |
| if cache_key in HistogramBinBoundaries.CACHE: |
| return HistogramBinBoundaries.CACHE[cache_key] |
| |
| bin_boundaries = HistogramBinBoundaries(dct[0]) |
| for slic in dct[1:]: |
| if not isinstance(slic, list): |
| bin_boundaries.AddBinBoundary(slic) |
| continue |
| if slic[0] == HistogramBinBoundaries.SLICE_TYPE_LINEAR: |
| bin_boundaries.AddLinearBins(slic[1], slic[2]) |
| elif slic[0] == HistogramBinBoundaries.SLICE_TYPE_EXPONENTIAL: |
| bin_boundaries.AddExponentialBins(slic[1], slic[2]) |
| else: |
| raise ValueError('Unrecognized HistogramBinBoundaries slice type') |
| |
| bin_boundaries._BuildBins() |
| HistogramBinBoundaries.CACHE[cache_key] = bin_boundaries |
| return bin_boundaries |
| |
| @staticmethod |
| def FromProto(proto): |
| if not proto: |
| return HistogramBinBoundaries.SINGULAR |
| |
| bin_boundaries = HistogramBinBoundaries(proto.first_bin_boundary) |
| for spec in proto.bin_specs: |
| if spec.HasField('bin_boundary'): |
| bin_boundaries.AddBinBoundary(spec.bin_boundary) |
| elif spec.HasField('bin_spec'): |
| bin_spec = spec.bin_spec |
| b = histogram_proto.Pb2().BinBoundaryDetailedSpec |
| if bin_spec.boundary_type == b.LINEAR: |
| bin_boundaries.AddLinearBins( |
| bin_spec.maximum_bin_boundary, bin_spec.num_bin_boundaries) |
| elif bin_spec.boundary_type == b.EXPONENTIAL: |
| bin_boundaries.AddExponentialBins( |
| bin_spec.maximum_bin_boundary, bin_spec.num_bin_boundaries) |
| else: |
| raise ValueError('Unrecognized HistogramBinBoundaries slice type') |
| |
| bin_boundaries._BuildBins() |
| return bin_boundaries |
| |
| @staticmethod |
| def ToProto(dct): |
| proto = histogram_proto.Pb2().BinBoundaries() |
| if dct is None: |
| proto.first_bin_boundary = JS_MAX_VALUE |
| return proto |
| |
| proto.first_bin_boundary = dct[0] |
| for slic in dct[1:]: |
| spec = proto.bin_specs.add() |
| if not isinstance(slic, list): |
| spec.bin_boundary = slic |
| continue |
| |
| b = histogram_proto.Pb2().BinBoundaryDetailedSpec |
| if slic[0] == HistogramBinBoundaries.SLICE_TYPE_LINEAR: |
| spec.bin_spec.boundary_type = b.LINEAR |
| spec.bin_spec.maximum_bin_boundary = slic[1] |
| spec.bin_spec.num_bin_boundaries = slic[2] |
| elif slic[0] == HistogramBinBoundaries.SLICE_TYPE_EXPONENTIAL: |
| spec.bin_spec.boundary_type = b.EXPONENTIAL |
| spec.bin_spec.maximum_bin_boundary = slic[1] |
| spec.bin_spec.num_bin_boundaries = slic[2] |
| else: |
| raise ValueError('Unrecognized HistogramBinBoundaries slice type') |
| |
| return proto |
| |
| def AsDict(self): |
| if len(self._builder) == 1 and self._builder[0] == JS_MAX_VALUE: |
| return None |
| return self._builder |
| |
| @staticmethod |
| def CreateExponential(lower, upper, num_bins): |
| bin_boundaries = HistogramBinBoundaries(lower) |
| bin_boundaries.AddExponentialBins(upper, num_bins) |
| bin_boundaries._BuildBins() |
| return bin_boundaries |
| |
| @staticmethod |
| def CreateLinear(lower, upper, num_bins): |
| bin_boundaries = HistogramBinBoundaries(lower) |
| bin_boundaries.AddLinearBins(upper, num_bins) |
| bin_boundaries._BuildBins() |
| return bin_boundaries |
| |
| def _PushBuilderSlice(self, slic): |
| self._builder += [slic] |
| |
| def AddBinBoundary(self, next_max_bin_boundary): |
| if next_max_bin_boundary <= self.range.max: |
| raise ValueError('The added max bin boundary must be larger than ' + |
| 'the current max boundary') |
| |
| self._bin_ranges = None |
| self._bins = None |
| |
| self._PushBuilderSlice(next_max_bin_boundary) |
| self.range.AddValue(next_max_bin_boundary) |
| return self |
| |
| def AddLinearBins(self, next_max_bin_boundary, bin_count): |
| if bin_count <= 0: |
| raise ValueError('Bin count must be positive') |
| if next_max_bin_boundary <= self.range.max: |
| raise ValueError('The new max bin boundary must be greater than ' + |
| 'the previous max bin boundary') |
| |
| self._bin_ranges = None |
| self._bins = None |
| |
| self._PushBuilderSlice([ |
| HistogramBinBoundaries.SLICE_TYPE_LINEAR, |
| next_max_bin_boundary, bin_count]) |
| self.range.AddValue(next_max_bin_boundary) |
| return self |
| |
| def AddExponentialBins(self, next_max_bin_boundary, bin_count): |
| if bin_count <= 0: |
| raise ValueError('Bin count must be positive') |
| if self.range.max <= 0: |
| raise ValueError('Current max bin boundary must be positive') |
| if self.range.max >= next_max_bin_boundary: |
| raise ValueError('The last added max boundary must be greater than ' + |
| 'the current max boundary boundary') |
| |
| self._bin_ranges = None |
| self._bins = None |
| |
| self._PushBuilderSlice([ |
| HistogramBinBoundaries.SLICE_TYPE_EXPONENTIAL, |
| next_max_bin_boundary, bin_count]) |
| self.range.AddValue(next_max_bin_boundary) |
| return self |
| |
| @property |
| def bins(self): |
| if self._bins is None: |
| self._BuildBins() |
| return self._bins |
| |
| def _BuildBins(self): |
| self._bins = [HistogramBin(r) for r in self.bin_ranges] |
| |
| @property |
| def bin_ranges(self): |
| if self._bin_ranges is None: |
| self._BuildBinRanges() |
| return self._bin_ranges |
| |
| def _BuildBinRanges(self): |
| if not isinstance(self._builder[0], numbers.Number): |
| raise ValueError('Invalid start of builder_') |
| |
| self._bin_ranges = [] |
| prev_boundary = self._builder[0] |
| if prev_boundary > -JS_MAX_VALUE: |
| # underflow bin |
| self._bin_ranges.append(Range.FromExplicitRange( |
| -JS_MAX_VALUE, prev_boundary)) |
| |
| for slic in self._builder[1:]: |
| if not isinstance(slic, list): |
| self._bin_ranges.append(Range.FromExplicitRange( |
| prev_boundary, slic)) |
| prev_boundary = slic |
| continue |
| |
| next_max_bin_boundary = float(slic[1]) |
| bin_count = slic[2] |
| slice_min_bin_boundary = float(prev_boundary) |
| |
| if slic[0] == self.SLICE_TYPE_LINEAR: |
| bin_width = (next_max_bin_boundary - prev_boundary) / bin_count |
| for i in range(1, bin_count): |
| boundary = slice_min_bin_boundary + (i * bin_width) |
| self._bin_ranges.append(Range.FromExplicitRange( |
| prev_boundary, boundary)) |
| prev_boundary = boundary |
| elif slic[0] == self.SLICE_TYPE_EXPONENTIAL: |
| bin_exponent_width = ( |
| math.log(next_max_bin_boundary / prev_boundary) / bin_count) |
| for i in range(1, bin_count): |
| boundary = slice_min_bin_boundary * math.exp(i * bin_exponent_width) |
| self._bin_ranges.append(Range.FromExplicitRange( |
| prev_boundary, boundary)) |
| prev_boundary = boundary |
| else: |
| raise ValueError('Unrecognized HistogramBinBoundaries slice type') |
| |
| self._bin_ranges.append(Range.FromExplicitRange( |
| prev_boundary, next_max_bin_boundary)) |
| prev_boundary = next_max_bin_boundary |
| |
| if prev_boundary < JS_MAX_VALUE: |
| # overflow bin |
| self._bin_ranges.append(Range.FromExplicitRange( |
| prev_boundary, JS_MAX_VALUE)) |
| |
| |
| HistogramBinBoundaries.SINGULAR = HistogramBinBoundaries(JS_MAX_VALUE) |
| |
| |
| # The JS version computes these values using tr.b.convertUnit, which is |
| # not implemented in Python, so we write them out here. |
| def _CreateMsAutoFormatBins(): |
| bins = [ |
| 2000, |
| 5000, |
| 10000, |
| 30000, |
| 60000, |
| 120000, |
| 300000, |
| 600000, |
| 1800000, |
| 3600000, |
| 7200000, |
| 21600000, |
| 43200000, |
| 86400000, |
| 604800000, |
| 2629743840, |
| 31556926080 |
| ] |
| |
| boundaries = HistogramBinBoundaries(0).AddBinBoundary(1).AddExponentialBins( |
| 1e3, 3) |
| |
| for b in bins: |
| boundaries.AddBinBoundary(b) |
| |
| return boundaries |
| |
| |
| DEFAULT_BOUNDARIES_FOR_UNIT = { |
| 'ms': HistogramBinBoundaries.CreateExponential(1e-3, 1e6, 100), |
| 'tsMs': HistogramBinBoundaries.CreateLinear(0, 1e10, 1000), |
| 'msBestFitFormat': _CreateMsAutoFormatBins(), |
| 'n%': HistogramBinBoundaries.CreateLinear(0, 1.0, 20), |
| 'sizeInBytes': HistogramBinBoundaries.CreateExponential(1, 1e12, 100), |
| 'bytesPerSecond': HistogramBinBoundaries.CreateExponential(1, 1e12, 100), |
| 'J': HistogramBinBoundaries.CreateExponential(1e-3, 1e3, 50), |
| 'W': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50), |
| 'A': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50), |
| 'Ah': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50), |
| 'V': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50), |
| 'Hz': HistogramBinBoundaries.CreateExponential(1e-3, 1, 50), |
| 'unitless': HistogramBinBoundaries.CreateExponential(1e-3, 1e3, 50), |
| 'count': HistogramBinBoundaries.CreateExponential(1, 1e3, 20), |
| 'sigma': HistogramBinBoundaries.CreateLinear(-5, 5, 50), |
| } |