Home | History | Annotate | Download | only in value
      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