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.
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
9 The core Value concept provides the basic functionality:
10 - association with a page, may be none
12 - importance tracking [whether a value will show up on a waterfall or output
14 - other metadata, such as a description of what was measured
15 - default conversion to scalar and string
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.
25 from telemetry.core import discover
26 from telemetry.core import util
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'
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'
43 """An abstract value produced by a telemetry page test.
45 def __init__(self, page, name, units, important, description):
46 """A generic Value object.
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
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
63 self.important = important
64 self.description = description
66 def IsMergableWith(self, that):
67 return (self.units == that.units and
68 type(self) == type(that) and
69 self.important == that.important)
72 def MergeLikeValuesFromSamePage(cls, values):
73 """Combines the provided list of values into a single compound value.
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.
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.
82 For instance, it may be given
83 [ScalarValue(page, 'a', 1), ScalarValue(page, 'a', 2)]
85 ListOfScalarValues(page, 'a', [1, 2])
87 raise NotImplementedError()
90 def MergeLikeValuesFromDifferentPages(cls, values,
91 group_by_name_suffix=False):
92 """Combines the provided values into a single compound value.
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)]
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.
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.
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.
112 raise NotImplementedError()
114 def _IsImportantGivenOutputIntent(self, output_context):
115 if output_context == PER_PAGE_RESULT_OUTPUT_CONTEXT:
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
122 def GetBuildbotDataType(self, output_context):
123 """Returns the buildbot's equivalent data_type.
125 This should be one of the values accepted by perf_tests_results_helper.py.
127 raise NotImplementedError()
129 def GetBuildbotValue(self):
130 """Returns the buildbot's equivalent value."""
131 raise NotImplementedError()
133 def GetChartAndTraceNameForPerPageResult(self):
134 chart_name, _ = _ConvertValueNameToChartAndTraceName(self.name)
135 trace_name = self.page.display_name
136 return chart_name, trace_name
139 def name_suffix(self):
140 """Returns the string after a . in the name, or the full name otherwise."""
142 return self.name.split('.', 1)[1]
146 def GetChartAndTraceNameForComputedSummaryResult(
148 chart_name, trace_name = (
149 _ConvertValueNameToChartAndTraceName(self.name))
151 return chart_name, trace_name + trace_tag
153 return chart_name, trace_name
155 def GetRepresentativeNumber(self):
156 """Gets a single scalar value that best-represents this value.
158 Returns None if not possible.
160 raise NotImplementedError()
162 def GetRepresentativeString(self):
163 """Gets a string value that best-represents this value.
165 Returns None if not possible.
167 raise NotImplementedError()
170 def GetJSONTypeName():
171 """Gets the typename for serialization to JSON using AsDict."""
172 raise NotImplementedError()
175 """Pre-serializes a value to a dict for output as JSON."""
176 return self._AsDictImpl()
178 def _AsDictImpl(self):
181 'type': self.GetJSONTypeName(),
183 'important': self.important
187 d['description'] = self.description
190 d['page_id'] = self.page.id
194 def AsDictWithoutBaseClassEntries(self):
195 full_dict = self.AsDict()
196 base_dict_keys = set(self._AsDictImpl().keys())
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])
203 def FromDict(value_dict, page_dict):
204 """Produces a value from a value dict and a page dict.
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.
211 value_dict: a dictionary produced by AsDict() on a value subclass.
212 page_dict: a dictionary mapping IDs to page objects.
214 return Value.ListOfValuesFromListOfDicts([value_dict], page_dict)[0]
217 def ListOfValuesFromListOfDicts(value_dicts, page_dict):
218 """Takes a list of value dicts to values.
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.
225 values: a list of value dicts produced by AsDict() on a value subclass.
226 page_dict: a dictionary mapping IDs to page objects.
228 value_dir = os.path.dirname(__file__)
229 value_classes = discover.DiscoverClasses(
230 value_dir, util.GetTelemetryDir(),
231 Value, index_by_class_name=True)
233 value_json_types = dict((value_classes[x].GetJSONTypeName(), x) for x in
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))
246 def GetConstructorKwArgs(value_dict, page_dict):
247 """Produces constructor arguments from a value dict and a page dict.
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
253 value_dict: a dictionary produced by AsDict() on a value subclass.
254 page_dict: a dictionary mapping IDs to page objects.
257 'name': value_dict['name'],
258 'units': value_dict['units']
261 description = value_dict.get('description', None)
263 d['description'] = description
265 d['description'] = None
267 page_id = value_dict.get('page_id', None)
269 d['page'] = page_dict[int(page_id)]
273 d['important'] = False
277 def ValueNameFromTraceAndChartName(trace_name, chart_name=None):
278 """Mangles a trace name plus optional chart name into a standard string.
280 A value might just be a bareword name, e.g. numPixels. In that case, its
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'.
287 assert trace_name != 'url', 'The name url cannot be used'
289 return '%s.%s' % (chart_name, trace_name)
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.')
295 def _ConvertValueNameToChartAndTraceName(value_name):
296 """Converts a value_name into the equivalent chart-trace name pair.
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.
302 This converts from the telemetry convention to the buildbot convention,
303 returning a 2-tuple (measurement_name, trace_name).
305 if '.' in value_name:
306 return value_name.split('.', 1)
308 return value_name, value_name