blob: 6b3a60f358a81665c257fd1fb3466ad5590e09f5 [file] [log] [blame]
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from collections import defaultdict
from telemetry.core.platform import tracing_category_filter
from telemetry.core.platform import tracing_options
from telemetry.page import page_test
from telemetry.timeline import model as model_module
from telemetry.value import trace
from telemetry.web_perf import timeline_interaction_record as tir_module
from telemetry.web_perf.metrics import responsiveness_metric
from telemetry.web_perf.metrics import smoothness
from telemetry.web_perf.metrics import layout
# TimelineBasedMeasurement considers all instrumentation as producing a single
# timeline. But, depending on the amount of instrumentation that is enabled,
# overhead increases. The user of the measurement must therefore chose between
# a few levels of instrumentation.
NO_OVERHEAD_LEVEL = 'no-overhead'
MINIMAL_OVERHEAD_LEVEL = 'minimal-overhead'
DEBUG_OVERHEAD_LEVEL = 'debug-overhead'
ALL_OVERHEAD_LEVELS = [
NO_OVERHEAD_LEVEL,
MINIMAL_OVERHEAD_LEVEL,
DEBUG_OVERHEAD_LEVEL
]
def _GetAllTimelineBasedMetrics():
# TODO(nednguyen): use discovery pattern to return all the instances of
# all TimelineBasedMetrics class in web_perf/metrics/ folder.
# This cannot be done until crbug.com/460208 is fixed.
return (smoothness.SmoothnessMetric(),
responsiveness_metric.ResponsivenessMetric(),
layout.LayoutMetric())
class InvalidInteractions(Exception):
pass
# TODO(nednguyen): Get rid of this results wrapper hack after we add interaction
# record to telemetry value system.
class _ResultsWrapper(object):
def __init__(self, results, label):
self._results = results
self._result_prefix = label
@property
def current_page(self):
return self._results.current_page
def _GetResultName(self, trace_name):
return '%s-%s' % (self._result_prefix, trace_name)
def AddValue(self, value):
value.name = self._GetResultName(value.name)
self._results.AddValue(value)
def _GetRendererThreadsToInteractionRecordsMap(model):
threads_to_records_map = defaultdict(list)
interaction_labels_of_previous_threads = set()
for curr_thread in model.GetAllThreads():
for event in curr_thread.async_slices:
# TODO(nduca): Add support for page-load interaction record.
if tir_module.IsTimelineInteractionRecord(event.name):
interaction = tir_module.TimelineInteractionRecord.FromAsyncEvent(event)
threads_to_records_map[curr_thread].append(interaction)
if interaction.label in interaction_labels_of_previous_threads:
raise InvalidInteractions(
'Interaction record label %s is duplicated on different '
'threads' % interaction.label)
if curr_thread in threads_to_records_map:
interaction_labels_of_previous_threads.update(
r.label for r in threads_to_records_map[curr_thread])
return threads_to_records_map
class _TimelineBasedMetrics(object):
def __init__(self, model, renderer_thread, interaction_records):
self._model = model
self._renderer_thread = renderer_thread
self._interaction_records = interaction_records
def AddResults(self, results):
interactions_by_label = defaultdict(list)
for i in self._interaction_records:
interactions_by_label[i.label].append(i)
for label, interactions in interactions_by_label.iteritems():
are_repeatable = [i.repeatable for i in interactions]
if not all(are_repeatable) and len(interactions) > 1:
raise InvalidInteractions('Duplicate unrepeatable interaction records '
'on the page')
wrapped_results = _ResultsWrapper(results, label)
self.UpdateResultsByMetric(interactions, wrapped_results)
def UpdateResultsByMetric(self, interactions, wrapped_results):
if not interactions:
return
for metric in _GetAllTimelineBasedMetrics():
metric.AddResults(self._model, self._renderer_thread,
interactions, wrapped_results)
class Options(object):
"""A class to be used to configure TimelineBasedMeasurement.
This is created and returned by
Benchmark.CreateTimelineBasedMeasurementOptions.
"""
def __init__(self, overhead_level=NO_OVERHEAD_LEVEL):
"""As the amount of instrumentation increases, so does the overhead.
The user of the measurement chooses the overhead level that is appropriate,
and the tracing is filtered accordingly.
overhead_level: Can either be a custom TracingCategoryFilter object or
one of NO_OVERHEAD_LEVEL, MINIMAL_OVERHEAD_LEVEL or
DEBUG_OVERHEAD_LEVEL.
"""
if (not isinstance(overhead_level,
tracing_category_filter.TracingCategoryFilter) and
overhead_level not in ALL_OVERHEAD_LEVELS):
raise Exception("Overhead level must be a TracingCategoryFilter object"
" or valid overhead level string."
" Given overhead level: %s" % overhead_level)
self._overhead_level = overhead_level
self._extra_category_filters = []
def ExtendTraceCategoryFilters(self, filters):
self._extra_category_filters.extend(filters)
@property
def extra_category_filters(self):
return self._extra_category_filters
@property
def overhead_level(self):
return self._overhead_level
class TimelineBasedMeasurement(object):
"""Collects multiple metrics based on their interaction records.
A timeline based measurement shifts the burden of what metrics to collect onto
the user story under test. Instead of the measurement
having a fixed set of values it collects, the user story being tested
issues (via javascript) an Interaction record into the user timing API that
describing what is happening at that time, as well as a standardized set
of flags describing the semantics of the work being done. The
TimelineBasedMeasurement object collects a trace that includes both these
interaction records, and a user-chosen amount of performance data using
Telemetry's various timeline-producing APIs, tracing especially.
It then passes the recorded timeline to different TimelineBasedMetrics based
on those flags. As an example, this allows a single user story run to produce
load timing data, smoothness data, critical jank information and overall cpu
usage information.
For information on how to mark up a page to work with
TimelineBasedMeasurement, refer to the
perf.metrics.timeline_interaction_record module.
"""
def __init__(self, options):
self._tbm_options = options
def WillRunUserStory(self, tracing_controller,
synthetic_delay_categories=None):
"""Configure and start tracing.
Args:
app: an app.App subclass instance.
synthetic_delay_categories: iterable of delays. For example:
['DELAY(cc.BeginMainFrame;0.014;alternating)']
where 'cc.BeginMainFrame' is a timeline event, 0.014 is the delay,
and 'alternating' is the mode.
"""
if not tracing_controller.IsChromeTracingSupported():
raise Exception('Not supported')
if isinstance(self._tbm_options.overhead_level,
tracing_category_filter.TracingCategoryFilter):
category_filter = self._tbm_options.overhead_level
else:
assert self._tbm_options.overhead_level in ALL_OVERHEAD_LEVELS, (
"Invalid TBM Overhead Level: %s" % self._tbm_options.overhead_level)
if self._tbm_options.overhead_level == NO_OVERHEAD_LEVEL:
category_filter = tracing_category_filter.CreateNoOverheadFilter()
elif self._tbm_options.overhead_level == MINIMAL_OVERHEAD_LEVEL:
category_filter = tracing_category_filter.CreateMinimalOverheadFilter()
else:
category_filter = tracing_category_filter.CreateDebugOverheadFilter()
for new_category_filter in self._tbm_options.extra_category_filters:
category_filter.AddIncludedCategory(new_category_filter)
# TODO(slamm): Move synthetic_delay_categories to the TBM options.
for delay in synthetic_delay_categories or []:
category_filter.AddSyntheticDelay(delay)
options = tracing_options.TracingOptions()
options.enable_chrome_trace = True
options.enable_platform_display_trace = True
tracing_controller.Start(options, category_filter)
def Measure(self, tracing_controller, results):
"""Collect all possible metrics and added them to results."""
trace_result = tracing_controller.Stop()
results.AddValue(trace.TraceValue(results.current_page, trace_result))
model = model_module.TimelineModel(trace_result)
threads_to_records_map = _GetRendererThreadsToInteractionRecordsMap(model)
for renderer_thread, interaction_records in (
threads_to_records_map.iteritems()):
meta_metrics = _TimelineBasedMetrics(
model, renderer_thread, interaction_records)
meta_metrics.AddResults(results)
def DidRunUserStory(self, tracing_controller):
if tracing_controller.is_tracing_running:
tracing_controller.Stop()
class TimelineBasedPageTest(page_test.PageTest):
"""Page test that collects metrics with TimelineBasedMeasurement."""
def __init__(self, tbm):
super(TimelineBasedPageTest, self).__init__('RunPageInteractions')
self._measurement = tbm
@property
def measurement(self):
return self._measurement
def WillNavigateToPage(self, page, tab):
tracing_controller = tab.browser.platform.tracing_controller
self._measurement.WillRunUserStory(
tracing_controller, page.GetSyntheticDelayCategories())
def ValidateAndMeasurePage(self, page, tab, results):
"""Collect all possible metrics and added them to results."""
tracing_controller = tab.browser.platform.tracing_controller
self._measurement.Measure(tracing_controller, results)
def CleanUpAfterPage(self, page, tab):
tracing_controller = tab.browser.platform.tracing_controller
self._measurement.DidRunUserStory(tracing_controller)