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.
7 from collections import defaultdict
9 from telemetry.core import util
10 from telemetry.core.platform import tracing_category_filter
11 from telemetry.page import page_test
12 from telemetry.timeline import model as model_module
13 from telemetry.value import string as string_value_module
14 from telemetry.web_perf import timeline_interaction_record as tir_module
15 from telemetry.web_perf.metrics import fast_metric
16 from telemetry.web_perf.metrics import responsiveness_metric
17 from telemetry.web_perf.metrics import smoothness
19 # TimelineBasedMeasurement considers all instrumentation as producing a single
20 # timeline. But, depending on the amount of instrumentation that is enabled,
21 # overhead increases. The user of the measurement must therefore chose between
22 # a few levels of instrumentation.
23 NO_OVERHEAD_LEVEL = 'no-overhead'
24 MINIMAL_OVERHEAD_LEVEL = 'minimal-overhead'
25 DEBUG_OVERHEAD_LEVEL = 'debug-overhead'
27 ALL_OVERHEAD_LEVELS = [
29 MINIMAL_OVERHEAD_LEVEL,
34 class InvalidInteractions(Exception):
38 def _GetMetricFromMetricType(metric_type):
39 if metric_type == tir_module.IS_FAST:
40 return fast_metric.FastMetric()
41 if metric_type == tir_module.IS_SMOOTH:
42 return smoothness.SmoothnessMetric()
43 if metric_type == tir_module.IS_RESPONSIVE:
44 return responsiveness_metric.ResponsivenessMetric()
45 raise Exception('Unrecognized metric type: %s' % metric_type)
48 # TODO(nednguyen): Get rid of this results wrapper hack after we add interaction
49 # record to telemetry value system.
50 class _ResultsWrapper(object):
51 def __init__(self, results, label):
52 self._results = results
53 self._result_prefix = label
56 def current_page(self):
57 return self._results.current_page
59 def _GetResultName(self, trace_name):
60 return '%s-%s' % (self._result_prefix, trace_name)
62 def AddValue(self, value):
63 value.name = self._GetResultName(value.name)
64 self._results.AddValue(value)
66 class _TimelineBasedMetrics(object):
67 def __init__(self, model, renderer_thread,
68 get_metric_from_metric_type_callback):
70 self._renderer_thread = renderer_thread
71 self._get_metric_from_metric_type_callback = \
72 get_metric_from_metric_type_callback
74 def FindTimelineInteractionRecords(self):
75 # TODO(nduca): Add support for page-load interaction record.
76 return [tir_module.TimelineInteractionRecord.FromAsyncEvent(event) for
77 event in self._renderer_thread.async_slices
78 if tir_module.IsTimelineInteractionRecord(event.name)]
80 def AddResults(self, results):
81 all_interactions = self.FindTimelineInteractionRecords()
82 if len(all_interactions) == 0:
83 raise InvalidInteractions('Expected at least one interaction record on '
86 interactions_by_label = defaultdict(list)
87 for i in all_interactions:
88 interactions_by_label[i.label].append(i)
90 for label, interactions in interactions_by_label.iteritems():
91 are_repeatable = [i.repeatable for i in interactions]
92 if not all(are_repeatable) and len(interactions) > 1:
93 raise InvalidInteractions('Duplicate unrepeatable interaction records '
95 wrapped_results = _ResultsWrapper(results, label)
96 self.UpdateResultsByMetric(interactions, wrapped_results)
98 def UpdateResultsByMetric(self, interactions, wrapped_results):
99 for metric_type in tir_module.METRICS:
100 # For each metric type, either all or none of the interactions should
102 interactions_with_metric = [i for i in interactions if
103 i.HasMetric(metric_type)]
104 if not interactions_with_metric:
106 if len(interactions_with_metric) != len(interactions):
107 raise InvalidInteractions('Interaction records with the same logical '
108 'name must have the same flags.')
109 metric = self._get_metric_from_metric_type_callback(metric_type)
110 metric.AddResults(self._model, self._renderer_thread,
111 interactions, wrapped_results)
114 class TimelineBasedMeasurement(page_test.PageTest):
115 """Collects multiple metrics pages based on their interaction records.
117 A timeline measurement shifts the burden of what metrics to collect onto the
118 page under test, or the pageset running that page. Instead of the measurement
119 having a fixed set of values it collects about the page, the page being tested
120 issues (via javascript) an Interaction record into the user timing API that
121 describing what the page is doing at that time, as well as a standardized set
122 of flags describing the semantics of the work being done. The
123 TimelineBasedMeasurement object collects a trace that includes both these
124 interaction recorsd, and a user-chosen amount of performance data using
125 Telemetry's various timeline-producing APIs, tracing especially.
127 It then passes the recorded timeline to different TimelineBasedMetrics based
128 on those flags. This allows a single run through a page to produce load timing
129 data, smoothness data, critical jank information and overall cpu usage
132 For information on how to mark up a page to work with
133 TimelineBasedMeasurement, refer to the
134 perf.metrics.timeline_interaction_record module.
138 super(TimelineBasedMeasurement, self).__init__('RunSmoothness')
141 def AddCommandLineArgs(cls, parser):
143 '--overhead-level', dest='overhead_level', type='choice',
144 choices=ALL_OVERHEAD_LEVELS,
145 default=NO_OVERHEAD_LEVEL,
146 help='How much overhead to incur during the measurement.')
148 '--trace-dir', dest='trace_dir', type='string', default=None,
149 help=('Where to save the trace after the run. If this flag '
150 'is not set, the trace will not be saved.'))
152 def WillNavigateToPage(self, page, tab):
153 if not tab.browser.supports_tracing:
154 raise Exception('Not supported')
156 assert self.options.overhead_level in ALL_OVERHEAD_LEVELS
157 if self.options.overhead_level == NO_OVERHEAD_LEVEL:
158 category_filter = tracing_category_filter.CreateNoOverheadFilter()
159 elif self.options.overhead_level == MINIMAL_OVERHEAD_LEVEL:
160 category_filter = tracing_category_filter.CreateMinimalOverheadFilter()
162 category_filter = tracing_category_filter.CreateDebugOverheadFilter()
164 for delay in page.GetSyntheticDelayCategories():
165 category_filter.AddSyntheticDelay(delay)
167 tab.browser.StartTracing(category_filter)
169 def ValidateAndMeasurePage(self, page, tab, results):
170 """ Collect all possible metrics and added them to results. """
171 trace_result = tab.browser.StopTracing()
172 trace_dir = self.options.trace_dir
174 trace_file_path = util.GetSequentialFileName(
175 os.path.join(trace_dir, 'trace')) + '.json'
177 with open(trace_file_path, 'w') as f:
178 trace_result.Serialize(f)
179 results.AddValue(string_value_module.StringValue(
180 page, 'trace_path', 'string', trace_file_path))
182 logging.error('Cannot open %s. %s' % (trace_file_path, e))
184 model = model_module.TimelineModel(trace_result)
185 renderer_thread = model.GetRendererThreadFromTabId(tab.id)
186 meta_metrics = _TimelineBasedMetrics(
187 model, renderer_thread, _GetMetricFromMetricType)
188 meta_metrics.AddResults(results)
190 def CleanUpAfterPage(self, page, tab):
191 if tab.browser.is_tracing_running:
192 tab.browser.StopTracing()