1# Copyright 2014 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4""" 5The Value hierarchy provides a way of representing the values measurements 6produce such that they can be merged across runs, grouped by page, and output 7to different targets. 8 9The core Value concept provides the basic functionality: 10- association with a page, may be none 11- naming and units 12- importance tracking [whether a value will show up on a waterfall or output 13 file by default] 14- other metadata, such as a description of what was measured 15- default conversion to scalar and string 16- merging properties 17 18A page may actually run a few times during a single telemetry session. 19Downstream consumers of test results typically want to group these runs 20together, then compute summary statistics across runs. Value provides the 21Merge* family of methods for this kind of aggregation. 22""" 23import os 24 25from telemetry.core import discover 26from telemetry.core import util 27 28# When converting a Value to its buildbot equivalent, the context in which the 29# value is being interpreted actually affects the conversion. This is insane, 30# but there you have it. There are three contexts in which Values are converted 31# for use by buildbot, represented by these output-intent values. 32PER_PAGE_RESULT_OUTPUT_CONTEXT = 'per-page-result-output-context' 33COMPUTED_PER_PAGE_SUMMARY_OUTPUT_CONTEXT = 'merged-pages-result-output-context' 34SUMMARY_RESULT_OUTPUT_CONTEXT = 'summary-result-output-context' 35 36class Value(object): 37 """An abstract value produced by a telemetry page test. 38 """ 39 def __init__(self, page, name, units, important, description, 40 tir_label, grouping_keys): 41 """A generic Value object. 42 43 Args: 44 page: A Page object, may be given as None to indicate that the value 45 represents results for multiple pages. 46 name: A value name string, may contain a dot. Values from the same test 47 with the same prefix before the dot may be considered to belong to 48 the same chart. 49 units: A units string. 50 important: Whether the value is "important". Causes the value to appear 51 by default in downstream UIs. 52 description: A string explaining in human-understandable terms what this 53 value represents. 54 tir_label: The string label of the TimelineInteractionRecord with 55 which this value is associated. 56 grouping_keys: A dict that maps grouping key names to grouping keys. 57 """ 58 # TODO(eakuefner): Check story here after migration (crbug.com/442036) 59 if not isinstance(name, basestring): 60 raise ValueError('name field of Value must be string.') 61 if not isinstance(units, basestring): 62 raise ValueError('units field of Value must be string.') 63 if not isinstance(important, bool): 64 raise ValueError('important field of Value must be bool.') 65 if not ((description is None) or isinstance(description, basestring)): 66 raise ValueError('description field of Value must absent or string.') 67 if not ((tir_label is None) or 68 isinstance(tir_label, basestring)): 69 raise ValueError('tir_label field of Value must absent or ' 70 'string.') 71 if not ((grouping_keys is None) or isinstance(grouping_keys, dict)): 72 raise ValueError('grouping_keys field of Value must be absent or dict') 73 74 if grouping_keys is None: 75 grouping_keys = {} 76 77 self.page = page 78 self.name = name 79 self.units = units 80 self.important = important 81 self.description = description 82 self.tir_label = tir_label 83 self.grouping_keys = grouping_keys 84 85 def __eq__(self, other): 86 return hash(self) == hash(other) 87 88 def __hash__(self): 89 return hash(str(self)) 90 91 def IsMergableWith(self, that): 92 return (self.units == that.units and 93 type(self) == type(that) and 94 self.important == that.important) 95 96 @classmethod 97 def MergeLikeValuesFromSamePage(cls, values): 98 """Combines the provided list of values into a single compound value. 99 100 When a page runs multiple times, it may produce multiple values. This 101 function is given the same-named values across the multiple runs, and has 102 the responsibility of producing a single result. 103 104 It must return a single Value. If merging does not make sense, the 105 implementation must pick a representative value from one of the runs. 106 107 For instance, it may be given 108 [ScalarValue(page, 'a', 1), ScalarValue(page, 'a', 2)] 109 and it might produce 110 ListOfScalarValues(page, 'a', [1, 2]) 111 """ 112 raise NotImplementedError() 113 114 @classmethod 115 def MergeLikeValuesFromDifferentPages(cls, values): 116 """Combines the provided values into a single compound value. 117 118 When a full pageset runs, a single value_name will usually end up getting 119 collected for multiple pages. For instance, we may end up with 120 [ScalarValue(page1, 'a', 1), 121 ScalarValue(page2, 'a', 2)] 122 123 This function takes in the values of the same name, but across multiple 124 pages, and produces a single summary result value. In this instance, it 125 could produce a ScalarValue(None, 'a', 1.5) to indicate averaging, or even 126 ListOfScalarValues(None, 'a', [1, 2]) if concatenated output was desired. 127 128 Some results are so specific to a page that they make no sense when 129 aggregated across pages. If merging values of this type across pages is 130 non-sensical, this method may return None. 131 """ 132 raise NotImplementedError() 133 134 def _IsImportantGivenOutputIntent(self, output_context): 135 if output_context == PER_PAGE_RESULT_OUTPUT_CONTEXT: 136 return False 137 elif output_context == COMPUTED_PER_PAGE_SUMMARY_OUTPUT_CONTEXT: 138 return self.important 139 elif output_context == SUMMARY_RESULT_OUTPUT_CONTEXT: 140 return self.important 141 142 def GetBuildbotDataType(self, output_context): 143 """Returns the buildbot's equivalent data_type. 144 145 This should be one of the values accepted by perf_tests_results_helper.py. 146 """ 147 raise NotImplementedError() 148 149 def GetBuildbotValue(self): 150 """Returns the buildbot's equivalent value.""" 151 raise NotImplementedError() 152 153 def GetChartAndTraceNameForPerPageResult(self): 154 chart_name, _ = _ConvertValueNameToChartAndTraceName(self.name) 155 trace_name = self.page.display_name 156 return chart_name, trace_name 157 158 @property 159 def name_suffix(self): 160 """Returns the string after a . in the name, or the full name otherwise.""" 161 if '.' in self.name: 162 return self.name.split('.', 1)[1] 163 else: 164 return self.name 165 166 def GetChartAndTraceNameForComputedSummaryResult( 167 self, trace_tag): 168 chart_name, trace_name = ( 169 _ConvertValueNameToChartAndTraceName(self.name)) 170 if trace_tag: 171 return chart_name, trace_name + trace_tag 172 else: 173 return chart_name, trace_name 174 175 def GetRepresentativeNumber(self): 176 """Gets a single scalar value that best-represents this value. 177 178 Returns None if not possible. 179 """ 180 raise NotImplementedError() 181 182 def GetRepresentativeString(self): 183 """Gets a string value that best-represents this value. 184 185 Returns None if not possible. 186 """ 187 raise NotImplementedError() 188 189 @staticmethod 190 def GetJSONTypeName(): 191 """Gets the typename for serialization to JSON using AsDict.""" 192 raise NotImplementedError() 193 194 def AsDict(self): 195 """Pre-serializes a value to a dict for output as JSON.""" 196 return self._AsDictImpl() 197 198 def _AsDictImpl(self): 199 d = { 200 'name': self.name, 201 'type': self.GetJSONTypeName(), 202 'units': self.units, 203 'important': self.important 204 } 205 206 if self.description: 207 d['description'] = self.description 208 209 if self.tir_label: 210 d['tir_label'] = self.tir_label 211 212 if self.page: 213 d['page_id'] = self.page.id 214 215 if self.grouping_keys: 216 d['grouping_keys'] = self.grouping_keys 217 218 return d 219 220 def AsDictWithoutBaseClassEntries(self): 221 full_dict = self.AsDict() 222 base_dict_keys = set(self._AsDictImpl().keys()) 223 224 # Extracts only entries added by the subclass. 225 return dict([(k, v) for (k, v) in full_dict.iteritems() 226 if k not in base_dict_keys]) 227 228 @staticmethod 229 def FromDict(value_dict, page_dict): 230 """Produces a value from a value dict and a page dict. 231 232 Value dicts are produced by serialization to JSON, and must be accompanied 233 by a dict mapping page IDs to pages, also produced by serialization, in 234 order to be completely deserialized. If deserializing multiple values, use 235 ListOfValuesFromListOfDicts instead. 236 237 value_dict: a dictionary produced by AsDict() on a value subclass. 238 page_dict: a dictionary mapping IDs to page objects. 239 """ 240 return Value.ListOfValuesFromListOfDicts([value_dict], page_dict)[0] 241 242 @staticmethod 243 def ListOfValuesFromListOfDicts(value_dicts, page_dict): 244 """Takes a list of value dicts to values. 245 246 Given a list of value dicts produced by AsDict, this method 247 deserializes the dicts given a dict mapping page IDs to pages. 248 This method performs memoization for deserializing a list of values 249 efficiently, where FromDict is meant to handle one-offs. 250 251 values: a list of value dicts produced by AsDict() on a value subclass. 252 page_dict: a dictionary mapping IDs to page objects. 253 """ 254 value_dir = os.path.dirname(__file__) 255 value_classes = discover.DiscoverClasses( 256 value_dir, util.GetTelemetryDir(), 257 Value, index_by_class_name=True) 258 259 value_json_types = dict((value_classes[x].GetJSONTypeName(), x) for x in 260 value_classes) 261 262 values = [] 263 for value_dict in value_dicts: 264 value_class = value_classes[value_json_types[value_dict['type']]] 265 assert 'FromDict' in value_class.__dict__, \ 266 'Subclass doesn\'t override FromDict' 267 values.append(value_class.FromDict(value_dict, page_dict)) 268 269 return values 270 271 @staticmethod 272 def GetConstructorKwArgs(value_dict, page_dict): 273 """Produces constructor arguments from a value dict and a page dict. 274 275 Takes a dict parsed from JSON and an index of pages and recovers the 276 keyword arguments to be passed to the constructor for deserializing the 277 dict. 278 279 value_dict: a dictionary produced by AsDict() on a value subclass. 280 page_dict: a dictionary mapping IDs to page objects. 281 """ 282 d = { 283 'name': value_dict['name'], 284 'units': value_dict['units'] 285 } 286 287 description = value_dict.get('description', None) 288 if description: 289 d['description'] = description 290 else: 291 d['description'] = None 292 293 page_id = value_dict.get('page_id', None) 294 if page_id is not None: 295 d['page'] = page_dict[int(page_id)] 296 else: 297 d['page'] = None 298 299 d['important'] = False 300 301 tir_label = value_dict.get('tir_label', None) 302 if tir_label: 303 d['tir_label'] = tir_label 304 else: 305 d['tir_label'] = None 306 307 grouping_keys = value_dict.get('grouping_keys', None) 308 if grouping_keys: 309 d['grouping_keys'] = grouping_keys 310 else: 311 d['grouping_keys'] = None 312 313 return d 314 315 316def MergedTirLabel(values): 317 """Returns the tir_label that should be applied to a merge of values. 318 319 As of TBMv2, we encounter situations where we need to merge values with 320 different tir_labels because Telemetry's tir_label field is being used to 321 store story keys for system health stories. As such, when merging, we want to 322 take the common tir_label if all values share the same label (legacy 323 behavior), or have no tir_label if not. 324 325 Args: 326 values: a list of Value instances 327 328 Returns: 329 The tir_label that would be set on the merge of |values|. 330 """ 331 assert len(values) > 0 332 v0 = values[0] 333 334 first_tir_label = v0.tir_label 335 if all(v.tir_label == first_tir_label for v in values): 336 return first_tir_label 337 else: 338 return None 339 340 341def ValueNameFromTraceAndChartName(trace_name, chart_name=None): 342 """Mangles a trace name plus optional chart name into a standard string. 343 344 A value might just be a bareword name, e.g. numPixels. In that case, its 345 chart may be None. 346 347 But, a value might also be intended for display with other values, in which 348 case the chart name indicates that grouping. So, you might have 349 screen.numPixels, screen.resolution, where chartName='screen'. 350 """ 351 assert trace_name != 'url', 'The name url cannot be used' 352 if chart_name: 353 return '%s.%s' % (chart_name, trace_name) 354 else: 355 assert '.' not in trace_name, ('Trace names cannot contain "." with an ' 356 'empty chart_name since this is used to delimit chart_name.trace_name.') 357 return trace_name 358 359 360def _ConvertValueNameToChartAndTraceName(value_name): 361 """Converts a value_name into the equivalent chart-trace name pair. 362 363 Buildbot represents values by the measurement name and an optional trace name, 364 whereas telemetry represents values with a chart_name.trace_name convention, 365 where chart_name is optional. This convention is also used by chart_json. 366 367 This converts from the telemetry convention to the buildbot convention, 368 returning a 2-tuple (measurement_name, trace_name). 369 """ 370 if '.' in value_name: 371 return value_name.split('.', 1) 372 else: 373 return value_name, value_name 374