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