|
1 # This Source Code Form is subject to the terms of the Mozilla Public |
|
2 # License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
|
4 |
|
5 import json |
|
6 import math |
|
7 import re |
|
8 |
|
9 from collections import OrderedDict |
|
10 |
|
11 def table_dispatch(kind, table, body): |
|
12 """Call body with table[kind] if it exists. Raise an error otherwise.""" |
|
13 if kind in table: |
|
14 return body(table[kind]) |
|
15 else: |
|
16 raise BaseException, "don't know how to handle a histogram of kind %s" % kind |
|
17 |
|
18 class DefinitionException(BaseException): |
|
19 pass |
|
20 |
|
21 def check_numeric_limits(dmin, dmax, n_buckets): |
|
22 if type(dmin) != int: |
|
23 raise DefinitionException, "minimum is not a number" |
|
24 if type(dmax) != int: |
|
25 raise DefinitionException, "maximum is not a number" |
|
26 if type(n_buckets) != int: |
|
27 raise DefinitionException, "number of buckets is not a number" |
|
28 |
|
29 def linear_buckets(dmin, dmax, n_buckets): |
|
30 check_numeric_limits(dmin, dmax, n_buckets) |
|
31 ret_array = [0] * n_buckets |
|
32 dmin = float(dmin) |
|
33 dmax = float(dmax) |
|
34 for i in range(1, n_buckets): |
|
35 linear_range = (dmin * (n_buckets - 1 - i) + dmax * (i - 1)) / (n_buckets - 2) |
|
36 ret_array[i] = int(linear_range + 0.5) |
|
37 return ret_array |
|
38 |
|
39 def exponential_buckets(dmin, dmax, n_buckets): |
|
40 check_numeric_limits(dmin, dmax, n_buckets) |
|
41 log_max = math.log(dmax); |
|
42 bucket_index = 2; |
|
43 ret_array = [0] * n_buckets |
|
44 current = dmin |
|
45 ret_array[1] = current |
|
46 for bucket_index in range(2, n_buckets): |
|
47 log_current = math.log(current) |
|
48 log_ratio = (log_max - log_current) / (n_buckets - bucket_index) |
|
49 log_next = log_current + log_ratio |
|
50 next_value = int(math.floor(math.exp(log_next) + 0.5)) |
|
51 if next_value > current: |
|
52 current = next_value |
|
53 else: |
|
54 current = current + 1 |
|
55 ret_array[bucket_index] = current |
|
56 return ret_array |
|
57 |
|
58 always_allowed_keys = ['kind', 'description', 'cpp_guard', 'expires_in_version'] |
|
59 |
|
60 class Histogram: |
|
61 """A class for representing a histogram definition.""" |
|
62 |
|
63 def __init__(self, name, definition): |
|
64 """Initialize a histogram named name with the given definition. |
|
65 definition is a dict-like object that must contain at least the keys: |
|
66 |
|
67 - 'kind': The kind of histogram. Must be one of 'boolean', 'flag', |
|
68 'enumerated', 'linear', or 'exponential'. |
|
69 - 'description': A textual description of the histogram. |
|
70 |
|
71 The key 'cpp_guard' is optional; if present, it denotes a preprocessor |
|
72 symbol that should guard C/C++ definitions associated with the histogram.""" |
|
73 self.verify_attributes(name, definition) |
|
74 self._name = name |
|
75 self._description = definition['description'] |
|
76 self._kind = definition['kind'] |
|
77 self._cpp_guard = definition.get('cpp_guard') |
|
78 self._extended_statistics_ok = definition.get('extended_statistics_ok', False) |
|
79 self._expiration = definition.get('expires_in_version') |
|
80 self.compute_bucket_parameters(definition) |
|
81 table = { 'boolean': 'BOOLEAN', |
|
82 'flag': 'FLAG', |
|
83 'enumerated': 'LINEAR', |
|
84 'linear': 'LINEAR', |
|
85 'exponential': 'EXPONENTIAL' } |
|
86 table_dispatch(self.kind(), table, |
|
87 lambda k: self._set_nsITelemetry_kind(k)) |
|
88 |
|
89 def name(self): |
|
90 """Return the name of the histogram.""" |
|
91 return self._name |
|
92 |
|
93 def description(self): |
|
94 """Return the description of the histogram.""" |
|
95 return self._description |
|
96 |
|
97 def kind(self): |
|
98 """Return the kind of the histogram. |
|
99 Will be one of 'boolean', 'flag', 'enumerated', 'linear', or 'exponential'.""" |
|
100 return self._kind |
|
101 |
|
102 def expiration(self): |
|
103 """Return the expiration version of the histogram.""" |
|
104 return self._expiration |
|
105 |
|
106 def nsITelemetry_kind(self): |
|
107 """Return the nsITelemetry constant corresponding to the kind of |
|
108 the histogram.""" |
|
109 return self._nsITelemetry_kind |
|
110 |
|
111 def _set_nsITelemetry_kind(self, kind): |
|
112 self._nsITelemetry_kind = "nsITelemetry::HISTOGRAM_%s" % kind |
|
113 |
|
114 def low(self): |
|
115 """Return the lower bound of the histogram. May be a string.""" |
|
116 return self._low |
|
117 |
|
118 def high(self): |
|
119 """Return the high bound of the histogram. May be a string.""" |
|
120 return self._high |
|
121 |
|
122 def n_buckets(self): |
|
123 """Return the number of buckets in the histogram. May be a string.""" |
|
124 return self._n_buckets |
|
125 |
|
126 def cpp_guard(self): |
|
127 """Return the preprocessor symbol that should guard C/C++ definitions |
|
128 associated with the histogram. Returns None if no guarding is necessary.""" |
|
129 return self._cpp_guard |
|
130 |
|
131 def extended_statistics_ok(self): |
|
132 """Return True if gathering extended statistics for this histogram |
|
133 is enabled.""" |
|
134 return self._extended_statistics_ok |
|
135 |
|
136 def ranges(self): |
|
137 """Return an array of lower bounds for each bucket in the histogram.""" |
|
138 table = { 'boolean': linear_buckets, |
|
139 'flag': linear_buckets, |
|
140 'enumerated': linear_buckets, |
|
141 'linear': linear_buckets, |
|
142 'exponential': exponential_buckets } |
|
143 return table_dispatch(self.kind(), table, |
|
144 lambda p: p(self.low(), self.high(), self.n_buckets())) |
|
145 |
|
146 def compute_bucket_parameters(self, definition): |
|
147 table = { |
|
148 'boolean': Histogram.boolean_flag_bucket_parameters, |
|
149 'flag': Histogram.boolean_flag_bucket_parameters, |
|
150 'enumerated': Histogram.enumerated_bucket_parameters, |
|
151 'linear': Histogram.linear_bucket_parameters, |
|
152 'exponential': Histogram.exponential_bucket_parameters |
|
153 } |
|
154 table_dispatch(self.kind(), table, |
|
155 lambda p: self.set_bucket_parameters(*p(definition))) |
|
156 |
|
157 def verify_attributes(self, name, definition): |
|
158 global always_allowed_keys |
|
159 general_keys = always_allowed_keys + ['low', 'high', 'n_buckets'] |
|
160 |
|
161 table = { |
|
162 'boolean': always_allowed_keys, |
|
163 'flag': always_allowed_keys, |
|
164 'enumerated': always_allowed_keys + ['n_values'], |
|
165 'linear': general_keys, |
|
166 'exponential': general_keys + ['extended_statistics_ok'] |
|
167 } |
|
168 table_dispatch(definition['kind'], table, |
|
169 lambda allowed_keys: Histogram.check_keys(name, definition, allowed_keys)) |
|
170 |
|
171 Histogram.check_expiration(name, definition) |
|
172 |
|
173 @staticmethod |
|
174 def check_expiration(name, definition): |
|
175 expiration = definition.get('expires_in_version') |
|
176 |
|
177 if not expiration: |
|
178 return |
|
179 |
|
180 if re.match(r'^[1-9][0-9]*$', expiration): |
|
181 expiration = expiration + ".0a1" |
|
182 elif re.match(r'^[1-9][0-9]*\.0$', expiration): |
|
183 expiration = expiration + "a1" |
|
184 |
|
185 definition['expires_in_version'] = expiration |
|
186 |
|
187 @staticmethod |
|
188 def check_keys(name, definition, allowed_keys): |
|
189 for key in definition.iterkeys(): |
|
190 if key not in allowed_keys: |
|
191 raise KeyError, '%s not permitted for %s' % (key, name) |
|
192 |
|
193 def set_bucket_parameters(self, low, high, n_buckets): |
|
194 def try_to_coerce_to_number(v): |
|
195 try: |
|
196 return eval(v, {}) |
|
197 except: |
|
198 return v |
|
199 self._low = try_to_coerce_to_number(low) |
|
200 self._high = try_to_coerce_to_number(high) |
|
201 self._n_buckets = try_to_coerce_to_number(n_buckets) |
|
202 |
|
203 @staticmethod |
|
204 def boolean_flag_bucket_parameters(definition): |
|
205 return (1, 2, 3) |
|
206 |
|
207 @staticmethod |
|
208 def linear_bucket_parameters(definition): |
|
209 return (definition.get('low', 1), |
|
210 definition['high'], |
|
211 definition['n_buckets']) |
|
212 |
|
213 @staticmethod |
|
214 def enumerated_bucket_parameters(definition): |
|
215 n_values = definition['n_values'] |
|
216 return (1, n_values, "%s+1" % n_values) |
|
217 |
|
218 @staticmethod |
|
219 def exponential_bucket_parameters(definition): |
|
220 return (definition.get('low', 1), |
|
221 definition['high'], |
|
222 definition['n_buckets']) |
|
223 |
|
224 def from_file(filename): |
|
225 """Return an iterator that provides a sequence of Histograms for |
|
226 the histograms defined in filename. |
|
227 """ |
|
228 with open(filename, 'r') as f: |
|
229 histograms = json.load(f, object_pairs_hook=OrderedDict) |
|
230 for (name, definition) in histograms.iteritems(): |
|
231 yield Histogram(name, definition) |