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