michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: import json michael@0: import math michael@0: import re michael@0: michael@0: from collections import OrderedDict michael@0: michael@0: def table_dispatch(kind, table, body): michael@0: """Call body with table[kind] if it exists. Raise an error otherwise.""" michael@0: if kind in table: michael@0: return body(table[kind]) michael@0: else: michael@0: raise BaseException, "don't know how to handle a histogram of kind %s" % kind michael@0: michael@0: class DefinitionException(BaseException): michael@0: pass michael@0: michael@0: def check_numeric_limits(dmin, dmax, n_buckets): michael@0: if type(dmin) != int: michael@0: raise DefinitionException, "minimum is not a number" michael@0: if type(dmax) != int: michael@0: raise DefinitionException, "maximum is not a number" michael@0: if type(n_buckets) != int: michael@0: raise DefinitionException, "number of buckets is not a number" michael@0: michael@0: def linear_buckets(dmin, dmax, n_buckets): michael@0: check_numeric_limits(dmin, dmax, n_buckets) michael@0: ret_array = [0] * n_buckets michael@0: dmin = float(dmin) michael@0: dmax = float(dmax) michael@0: for i in range(1, n_buckets): michael@0: linear_range = (dmin * (n_buckets - 1 - i) + dmax * (i - 1)) / (n_buckets - 2) michael@0: ret_array[i] = int(linear_range + 0.5) michael@0: return ret_array michael@0: michael@0: def exponential_buckets(dmin, dmax, n_buckets): michael@0: check_numeric_limits(dmin, dmax, n_buckets) michael@0: log_max = math.log(dmax); michael@0: bucket_index = 2; michael@0: ret_array = [0] * n_buckets michael@0: current = dmin michael@0: ret_array[1] = current michael@0: for bucket_index in range(2, n_buckets): michael@0: log_current = math.log(current) michael@0: log_ratio = (log_max - log_current) / (n_buckets - bucket_index) michael@0: log_next = log_current + log_ratio michael@0: next_value = int(math.floor(math.exp(log_next) + 0.5)) michael@0: if next_value > current: michael@0: current = next_value michael@0: else: michael@0: current = current + 1 michael@0: ret_array[bucket_index] = current michael@0: return ret_array michael@0: michael@0: always_allowed_keys = ['kind', 'description', 'cpp_guard', 'expires_in_version'] michael@0: michael@0: class Histogram: michael@0: """A class for representing a histogram definition.""" michael@0: michael@0: def __init__(self, name, definition): michael@0: """Initialize a histogram named name with the given definition. michael@0: definition is a dict-like object that must contain at least the keys: michael@0: michael@0: - 'kind': The kind of histogram. Must be one of 'boolean', 'flag', michael@0: 'enumerated', 'linear', or 'exponential'. michael@0: - 'description': A textual description of the histogram. michael@0: michael@0: The key 'cpp_guard' is optional; if present, it denotes a preprocessor michael@0: symbol that should guard C/C++ definitions associated with the histogram.""" michael@0: self.verify_attributes(name, definition) michael@0: self._name = name michael@0: self._description = definition['description'] michael@0: self._kind = definition['kind'] michael@0: self._cpp_guard = definition.get('cpp_guard') michael@0: self._extended_statistics_ok = definition.get('extended_statistics_ok', False) michael@0: self._expiration = definition.get('expires_in_version') michael@0: self.compute_bucket_parameters(definition) michael@0: table = { 'boolean': 'BOOLEAN', michael@0: 'flag': 'FLAG', michael@0: 'enumerated': 'LINEAR', michael@0: 'linear': 'LINEAR', michael@0: 'exponential': 'EXPONENTIAL' } michael@0: table_dispatch(self.kind(), table, michael@0: lambda k: self._set_nsITelemetry_kind(k)) michael@0: michael@0: def name(self): michael@0: """Return the name of the histogram.""" michael@0: return self._name michael@0: michael@0: def description(self): michael@0: """Return the description of the histogram.""" michael@0: return self._description michael@0: michael@0: def kind(self): michael@0: """Return the kind of the histogram. michael@0: Will be one of 'boolean', 'flag', 'enumerated', 'linear', or 'exponential'.""" michael@0: return self._kind michael@0: michael@0: def expiration(self): michael@0: """Return the expiration version of the histogram.""" michael@0: return self._expiration michael@0: michael@0: def nsITelemetry_kind(self): michael@0: """Return the nsITelemetry constant corresponding to the kind of michael@0: the histogram.""" michael@0: return self._nsITelemetry_kind michael@0: michael@0: def _set_nsITelemetry_kind(self, kind): michael@0: self._nsITelemetry_kind = "nsITelemetry::HISTOGRAM_%s" % kind michael@0: michael@0: def low(self): michael@0: """Return the lower bound of the histogram. May be a string.""" michael@0: return self._low michael@0: michael@0: def high(self): michael@0: """Return the high bound of the histogram. May be a string.""" michael@0: return self._high michael@0: michael@0: def n_buckets(self): michael@0: """Return the number of buckets in the histogram. May be a string.""" michael@0: return self._n_buckets michael@0: michael@0: def cpp_guard(self): michael@0: """Return the preprocessor symbol that should guard C/C++ definitions michael@0: associated with the histogram. Returns None if no guarding is necessary.""" michael@0: return self._cpp_guard michael@0: michael@0: def extended_statistics_ok(self): michael@0: """Return True if gathering extended statistics for this histogram michael@0: is enabled.""" michael@0: return self._extended_statistics_ok michael@0: michael@0: def ranges(self): michael@0: """Return an array of lower bounds for each bucket in the histogram.""" michael@0: table = { 'boolean': linear_buckets, michael@0: 'flag': linear_buckets, michael@0: 'enumerated': linear_buckets, michael@0: 'linear': linear_buckets, michael@0: 'exponential': exponential_buckets } michael@0: return table_dispatch(self.kind(), table, michael@0: lambda p: p(self.low(), self.high(), self.n_buckets())) michael@0: michael@0: def compute_bucket_parameters(self, definition): michael@0: table = { michael@0: 'boolean': Histogram.boolean_flag_bucket_parameters, michael@0: 'flag': Histogram.boolean_flag_bucket_parameters, michael@0: 'enumerated': Histogram.enumerated_bucket_parameters, michael@0: 'linear': Histogram.linear_bucket_parameters, michael@0: 'exponential': Histogram.exponential_bucket_parameters michael@0: } michael@0: table_dispatch(self.kind(), table, michael@0: lambda p: self.set_bucket_parameters(*p(definition))) michael@0: michael@0: def verify_attributes(self, name, definition): michael@0: global always_allowed_keys michael@0: general_keys = always_allowed_keys + ['low', 'high', 'n_buckets'] michael@0: michael@0: table = { michael@0: 'boolean': always_allowed_keys, michael@0: 'flag': always_allowed_keys, michael@0: 'enumerated': always_allowed_keys + ['n_values'], michael@0: 'linear': general_keys, michael@0: 'exponential': general_keys + ['extended_statistics_ok'] michael@0: } michael@0: table_dispatch(definition['kind'], table, michael@0: lambda allowed_keys: Histogram.check_keys(name, definition, allowed_keys)) michael@0: michael@0: Histogram.check_expiration(name, definition) michael@0: michael@0: @staticmethod michael@0: def check_expiration(name, definition): michael@0: expiration = definition.get('expires_in_version') michael@0: michael@0: if not expiration: michael@0: return michael@0: michael@0: if re.match(r'^[1-9][0-9]*$', expiration): michael@0: expiration = expiration + ".0a1" michael@0: elif re.match(r'^[1-9][0-9]*\.0$', expiration): michael@0: expiration = expiration + "a1" michael@0: michael@0: definition['expires_in_version'] = expiration michael@0: michael@0: @staticmethod michael@0: def check_keys(name, definition, allowed_keys): michael@0: for key in definition.iterkeys(): michael@0: if key not in allowed_keys: michael@0: raise KeyError, '%s not permitted for %s' % (key, name) michael@0: michael@0: def set_bucket_parameters(self, low, high, n_buckets): michael@0: def try_to_coerce_to_number(v): michael@0: try: michael@0: return eval(v, {}) michael@0: except: michael@0: return v michael@0: self._low = try_to_coerce_to_number(low) michael@0: self._high = try_to_coerce_to_number(high) michael@0: self._n_buckets = try_to_coerce_to_number(n_buckets) michael@0: michael@0: @staticmethod michael@0: def boolean_flag_bucket_parameters(definition): michael@0: return (1, 2, 3) michael@0: michael@0: @staticmethod michael@0: def linear_bucket_parameters(definition): michael@0: return (definition.get('low', 1), michael@0: definition['high'], michael@0: definition['n_buckets']) michael@0: michael@0: @staticmethod michael@0: def enumerated_bucket_parameters(definition): michael@0: n_values = definition['n_values'] michael@0: return (1, n_values, "%s+1" % n_values) michael@0: michael@0: @staticmethod michael@0: def exponential_bucket_parameters(definition): michael@0: return (definition.get('low', 1), michael@0: definition['high'], michael@0: definition['n_buckets']) michael@0: michael@0: def from_file(filename): michael@0: """Return an iterator that provides a sequence of Histograms for michael@0: the histograms defined in filename. michael@0: """ michael@0: with open(filename, 'r') as f: michael@0: histograms = json.load(f, object_pairs_hook=OrderedDict) michael@0: for (name, definition) in histograms.iteritems(): michael@0: yield Histogram(name, definition)