1# Copyright 2013 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- default conversion to scalar and string
15- merging properties
16
17A page may actually run a few times during a single telemetry session.
18Downstream consumers of test results typically want to group these runs
19together, then compute summary statistics across runs. Value provides the
20Merge* family of methods for this kind of aggregation.
21"""
22
23# When combining a pair of Values togehter, it is sometimes ambiguous whether
24# the values should be concatenated, or one should be picked as representative.
25# The possible merging policies are listed here.
26CONCATENATE = 'concatenate'
27PICK_FIRST = 'pick-first'
28
29# When converting a Value to its buildbot equivalent, the context in which the
30# value is being interpreted actually affects the conversion. This is insane,
31# but there you have it. There are three contexts in which Values are converted
32# for use by buildbot, represented by these output-intent values.
33PER_PAGE_RESULT_OUTPUT_CONTEXT = 'per-page-result-output-context'
34MERGED_PAGES_RESULT_OUTPUT_CONTEXT = 'merged-pages-result-output-context'
35SUMMARY_RESULT_OUTPUT_CONTEXT = 'summary-result-output-context'
36
37class Value(object):
38  """An abstract value produced by a telemetry page test.
39  """
40  def __init__(self, page, name, units, important):
41    """A generic Value object.
42
43    Note: page may be given as None to indicate that the value represents
44    results multiple pages.
45    """
46    self.page = page
47    self.name = name
48    self.units = units
49    self.important = important
50
51  def IsMergableWith(self, that):
52    return (self.units == that.units and
53            type(self) == type(that) and
54            self.important == that.important)
55
56  @classmethod
57  def MergeLikeValuesFromSamePage(cls, values):
58    """Combines the provided list of values into a single compound value.
59
60    When a page runs multiple times, it may produce multiple values. This
61    function is given the same-named values across the multiple runs, and has
62    the responsibility of producing a single result.
63
64    It must return a single Value. If merging does not make sense, the
65    implementation must pick a representative value from one of the runs.
66
67    For instance, it may be given
68        [ScalarValue(page, 'a', 1), ScalarValue(page, 'a', 2)]
69    and it might produce
70        ListOfScalarValues(page, 'a', [1, 2])
71    """
72    raise NotImplementedError()
73
74  @classmethod
75  def MergeLikeValuesFromDifferentPages(cls, values,
76                                        group_by_name_suffix=False):
77    """Combines the provided values into a single compound value.
78
79    When a full pageset runs, a single value_name will usually end up getting
80    collected for multiple pages. For instance, we may end up with
81       [ScalarValue(page1, 'a',  1),
82        ScalarValue(page2, 'a',  2)]
83
84    This function takes in the values of the same name, but across multiple
85    pages, and produces a single summary result value. In this instance, it
86    could produce a ScalarValue(None, 'a', 1.5) to indicate averaging, or even
87    ListOfScalarValues(None, 'a', [1, 2]) if concatenated output was desired.
88
89    Some results are so specific to a page that they make no sense when
90    aggregated across pages. If merging values of this type across pages is
91    non-sensical, this method may return None.
92
93    If group_by_name_suffix is True, then x.z and y.z are considered to be the
94    same value and are grouped together. If false, then x.z and y.z are
95    considered different.
96    """
97    raise NotImplementedError()
98
99  def _IsImportantGivenOutputIntent(self, output_context):
100    if output_context == PER_PAGE_RESULT_OUTPUT_CONTEXT:
101      return False
102    elif output_context == MERGED_PAGES_RESULT_OUTPUT_CONTEXT:
103      return self.important
104    elif output_context == SUMMARY_RESULT_OUTPUT_CONTEXT:
105      return self.important
106
107  def GetBuildbotDataType(self, output_context):
108    """Returns the buildbot's equivalent data_type.
109
110    This should be one of the values accepted by perf_tests_results_helper.py.
111    """
112    raise NotImplementedError()
113
114  def GetBuildbotValue(self):
115    """Returns the buildbot's equivalent value."""
116    raise NotImplementedError()
117
118  def GetBuildbotMeasurementAndTraceNameForPerPageResult(self):
119    measurement, _ = (
120       _ConvertValueNameToBuildbotChartAndTraceName(self.name))
121    return measurement + '_by_url', self.page.display_name
122
123  @property
124  def name_suffix(self):
125    """Returns the string after a . in the name, or the full name otherwise."""
126    if '.' in self.name:
127      return self.name.split('.', 1)[1]
128    else:
129      return self.name
130
131  def GetBuildbotMeasurementAndTraceNameForMergedPagesResult(self, trace_tag):
132    measurement, bb_trace_name = (
133        _ConvertValueNameToBuildbotChartAndTraceName(self.name))
134    if trace_tag:
135      return measurement, bb_trace_name + trace_tag
136    else:
137      return measurement, bb_trace_name
138
139  def GetRepresentativeNumber(self):
140    """Gets a single scalar value that best-represents this value.
141
142    Returns None if not possible.
143    """
144    raise NotImplementedError()
145
146  def GetRepresentativeString(self):
147    """Gets a string value that best-represents this value.
148
149    Returns None if not possible.
150    """
151    raise NotImplementedError()
152
153def ValueNameFromTraceAndChartName(trace_name, chart_name=None):
154  """Mangles a trace name plus optional chart name into a standard string.
155
156  A value might just be a bareword name, e.g. numPixels. In that case, its
157  chart may be None.
158
159  But, a value might also be intended for display with other values, in which
160  case the chart name indicates that grouping. So, you might have
161  screen.numPixels, screen.resolution, where chartName='screen'.
162  """
163  assert trace_name != 'url', 'The name url cannot be used'
164  if chart_name:
165    return '%s.%s' % (chart_name, trace_name)
166  else:
167    return trace_name
168
169def _ConvertValueNameToBuildbotChartAndTraceName(value_name):
170  """Converts a value_name into the buildbot equivalent name pair.
171
172  Buildbot represents values by the measurement name and an optional trace name,
173  whereas telemetry represents values with a chart_name.trace_name convention,
174  where chart_name is optional.
175
176  This converts from the telemetry convention to the buildbot convention,
177  returning a 2-tuple (measurement_name, trace_name).
178  """
179  if '.' in value_name:
180    return value_name.split('.', 1)
181  else:
182    return value_name, value_name
183