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