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               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
321def 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
339def _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