Commit c50eb076 authored by ariblue@google.com's avatar ariblue@google.com

Handle multiple duplicate timeline interaction records.

BUG=

Review URL: https://codereview.chromium.org/367523002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@282739 0039d316-1c4b-4281-b951-d872f2087c98
parent f57c7a53
...@@ -29,7 +29,8 @@ class ActionRunner(object): ...@@ -29,7 +29,8 @@ class ActionRunner(object):
action.WillRunAction(self._tab) action.WillRunAction(self._tab)
action.RunAction(self._tab) action.RunAction(self._tab)
def BeginInteraction(self, label, is_smooth=False, is_responsive=False): def BeginInteraction(self, label, is_smooth=False, is_responsive=False,
repeatable=False):
"""Marks the beginning of an interaction record. """Marks the beginning of an interaction record.
An interaction record is a labeled time period containing An interaction record is a labeled time period containing
...@@ -44,19 +45,24 @@ class ActionRunner(object): ...@@ -44,19 +45,24 @@ class ActionRunner(object):
is_smooth: Whether to check for smoothness metrics for this interaction. is_smooth: Whether to check for smoothness metrics for this interaction.
is_responsive: Whether to check for responsiveness metrics for is_responsive: Whether to check for responsiveness metrics for
this interaction. this interaction.
repeatable: Whether other interactions may use the same logical name
as this interaction. All interactions with the same logical name must
have the same flags.
""" """
flags = [] flags = []
if is_smooth: if is_smooth:
flags.append(tir_module.IS_SMOOTH) flags.append(tir_module.IS_SMOOTH)
if is_responsive: if is_responsive:
flags.append(tir_module.IS_RESPONSIVE) flags.append(tir_module.IS_RESPONSIVE)
if repeatable:
flags.append(tir_module.REPEATABLE)
interaction = Interaction(self._tab, label, flags) interaction = Interaction(self._tab, label, flags)
interaction.Begin() interaction.Begin()
return interaction return interaction
def BeginGestureInteraction( def BeginGestureInteraction(self, label, is_smooth=False, is_responsive=False,
self, label, is_smooth=False, is_responsive=False): repeatable=False):
"""Marks the beginning of a gesture-based interaction record. """Marks the beginning of a gesture-based interaction record.
This is similar to normal interaction record, but it will This is similar to normal interaction record, but it will
...@@ -73,8 +79,12 @@ class ActionRunner(object): ...@@ -73,8 +79,12 @@ class ActionRunner(object):
is_smooth: Whether to check for smoothness metrics for this interaction. is_smooth: Whether to check for smoothness metrics for this interaction.
is_responsive: Whether to check for responsiveness metrics for is_responsive: Whether to check for responsiveness metrics for
this interaction. this interaction.
repeatable: Whether other interactions may use the same logical name
as this interaction. All interactions with the same logical name must
have the same flags.
""" """
return self.BeginInteraction('Gesture_' + label, is_smooth, is_responsive) return self.BeginInteraction('Gesture_' + label, is_smooth, is_responsive,
repeatable)
def NavigateToPage(self, page, timeout_in_seconds=60): def NavigateToPage(self, page, timeout_in_seconds=60):
"""Navigate to the given page. """Navigate to the given page.
......
...@@ -5,12 +5,13 @@ ...@@ -5,12 +5,13 @@
import logging import logging
import os import os
from collections import defaultdict
from telemetry.core import util from telemetry.core import util
from telemetry.core.backends.chrome import tracing_backend from telemetry.core.backends.chrome import tracing_backend
from telemetry.timeline import model as model_module from telemetry.timeline import model as model_module
from telemetry.web_perf import timeline_interaction_record as tir_module from telemetry.web_perf import timeline_interaction_record as tir_module
from telemetry.web_perf.metrics import smoothness
from telemetry.web_perf.metrics import responsiveness_metric from telemetry.web_perf.metrics import responsiveness_metric
from telemetry.web_perf.metrics import smoothness
from telemetry.page import page_measurement from telemetry.page import page_measurement
from telemetry.value import string as string_value_module from telemetry.value import string as string_value_module
...@@ -30,28 +31,43 @@ ALL_OVERHEAD_LEVELS = [ ...@@ -30,28 +31,43 @@ ALL_OVERHEAD_LEVELS = [
] ]
class InvalidInteractions(Exception):
pass
def _GetMetricFromMetricType(metric_type):
if metric_type == tir_module.IS_SMOOTH:
return smoothness.SmoothnessMetric()
if metric_type == tir_module.IS_RESPONSIVE:
return responsiveness_metric.ResponsivenessMetric()
raise Exception('Unrecognized metric type: %s' % metric_type)
class _ResultsWrapper(object): class _ResultsWrapper(object):
def __init__(self, results, interaction_record): def __init__(self, results, logical_name):
self._results = results self._results = results
self._interaction_record = interaction_record self._result_prefix = logical_name
def _GetResultName(self, trace_name):
return '%s-%s' % (self._result_prefix, trace_name)
def Add(self, trace_name, units, value, chart_name=None, data_type='default'): def Add(self, trace_name, units, value, chart_name=None, data_type='default'):
trace_name = self._interaction_record.GetResultNameFor(trace_name) result_name = self._GetResultName(trace_name)
self._results.Add(trace_name, units, value, chart_name, data_type) self._results.Add(result_name, units, value, chart_name, data_type)
def AddSummary(self, trace_name, units, value, chart_name=None, def AddSummary(self, trace_name, units, value, chart_name=None,
data_type='default'): data_type='default'):
trace_name = self._interaction_record.GetResultNameFor(trace_name) result_name = self._GetResultName(trace_name)
self._results.AddSummary(trace_name, units, value, chart_name, data_type) self._results.AddSummary(result_name, units, value, chart_name, data_type)
class _TimelineBasedMetrics(object): class _TimelineBasedMetrics(object):
def __init__(self, model, renderer_thread, def __init__(self, model, renderer_thread,
create_metrics_for_interaction_record_callback): get_metric_from_metric_type_callback):
self._model = model self._model = model
self._renderer_thread = renderer_thread self._renderer_thread = renderer_thread
self._create_metrics_for_interaction_record_callback = \ self._get_metric_from_metric_type_callback = \
create_metrics_for_interaction_record_callback get_metric_from_metric_type_callback
def FindTimelineInteractionRecords(self): def FindTimelineInteractionRecords(self):
# TODO(nduca): Add support for page-load interaction record. # TODO(nduca): Add support for page-load interaction record.
...@@ -60,16 +76,37 @@ class _TimelineBasedMetrics(object): ...@@ -60,16 +76,37 @@ class _TimelineBasedMetrics(object):
if tir_module.IsTimelineInteractionRecord(event.name)] if tir_module.IsTimelineInteractionRecord(event.name)]
def AddResults(self, results): def AddResults(self, results):
interactions = self.FindTimelineInteractionRecords() all_interactions = self.FindTimelineInteractionRecords()
if len(interactions) == 0: if len(all_interactions) == 0:
raise Exception('Expected at least one Interaction on the page') raise InvalidInteractions('Expected at least one interaction record on '
for interaction in interactions: 'the page')
metrics = \
self._create_metrics_for_interaction_record_callback(interaction) interactions_by_logical_name = defaultdict(list)
wrapped_results = _ResultsWrapper(results, interaction) for i in all_interactions:
for m in metrics: interactions_by_logical_name[i.logical_name].append(i)
m.AddResults(self._model, self._renderer_thread,
[interaction], wrapped_results) for logical_name, interactions in interactions_by_logical_name.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, logical_name)
self.UpdateResultsByMetric(interactions, wrapped_results)
def UpdateResultsByMetric(self, interactions, wrapped_results):
for metric_type in tir_module.METRICS:
# For each metric type, either all or none of the interactions should
# have that metric.
interactions_with_metric = [i for i in interactions if
i.HasMetric(metric_type)]
if not interactions_with_metric:
continue
if len(interactions_with_metric) != len(interactions):
raise InvalidInteractions('Interaction records with the same logical '
'name must have the same flags.')
metric = self._get_metric_from_metric_type_callback(metric_type)
metric.AddResults(self._model, self._renderer_thread,
interactions, wrapped_results)
class TimelineBasedMeasurement(page_measurement.PageMeasurement): class TimelineBasedMeasurement(page_measurement.PageMeasurement):
...@@ -124,17 +161,6 @@ class TimelineBasedMeasurement(page_measurement.PageMeasurement): ...@@ -124,17 +161,6 @@ class TimelineBasedMeasurement(page_measurement.PageMeasurement):
categories = ','.join([categories] + page.GetSyntheticDelayCategories()) categories = ','.join([categories] + page.GetSyntheticDelayCategories())
tab.browser.StartTracing(categories) tab.browser.StartTracing(categories)
def CreateMetricsForTimelineInteractionRecord(self, interaction):
""" Subclass of TimelineBasedMeasurement overrides this method to customize
the binding of interaction's flags to metrics.
"""
res = []
if interaction.is_smooth:
res.append(smoothness.SmoothnessMetric())
if interaction.is_responsive:
res.append(responsiveness_metric.ResponsivenessMetric())
return res
def MeasurePage(self, page, tab, results): def MeasurePage(self, page, tab, results):
""" Collect all possible metrics and added them to results. """ """ Collect all possible metrics and added them to results. """
trace_result = tab.browser.StopTracing() trace_result = tab.browser.StopTracing()
...@@ -153,7 +179,7 @@ class TimelineBasedMeasurement(page_measurement.PageMeasurement): ...@@ -153,7 +179,7 @@ class TimelineBasedMeasurement(page_measurement.PageMeasurement):
model = model_module.TimelineModel(trace_result) model = model_module.TimelineModel(trace_result)
renderer_thread = model.GetRendererThreadFromTabId(tab.id) renderer_thread = model.GetRendererThreadFromTabId(tab.id)
meta_metrics = _TimelineBasedMetrics( meta_metrics = _TimelineBasedMetrics(
model, renderer_thread, self.CreateMetricsForTimelineInteractionRecord) model, renderer_thread, _GetMetricFromMetricType)
meta_metrics.AddResults(results) meta_metrics.AddResults(results)
def CleanUpAfterPage(self, page, tab): def CleanUpAfterPage(self, page, tab):
......
...@@ -7,96 +7,145 @@ import unittest ...@@ -7,96 +7,145 @@ import unittest
from telemetry import benchmark from telemetry import benchmark
from telemetry.core import wpr_modes from telemetry.core import wpr_modes
from telemetry.timeline import model as model_module from telemetry.page import page as page_module
from telemetry.timeline import async_slice
from telemetry.page import page_measurement_unittest_base from telemetry.page import page_measurement_unittest_base
from telemetry.page import page_set from telemetry.page import page_set
from telemetry.page import page as page_module from telemetry.page import page as page_module
from telemetry.results import page_measurement_results from telemetry.results import page_measurement_results
from telemetry.timeline import async_slice
from telemetry.timeline import model as model_module
from telemetry.unittest import options_for_unittests from telemetry.unittest import options_for_unittests
from telemetry.web_perf import timeline_based_measurement as tbm_module from telemetry.web_perf import timeline_based_measurement as tbm_module
from telemetry.web_perf import timeline_interaction_record as tir_module
from telemetry.web_perf.metrics import timeline_based_metric from telemetry.web_perf.metrics import timeline_based_metric
class TimelineBasedMetricsTests(unittest.TestCase): class FakeSmoothMetric(timeline_based_metric.TimelineBasedMetric):
def setUp(self): def AddResults(self, model, renderer_thread,
model = model_module.TimelineModel() interaction_records, results):
renderer_thread = model.GetOrCreateProcess(1).GetOrCreateThread(2) results.Add('FakeSmoothMetric', 'ms', 1)
renderer_thread.name = 'CrRendererMain' results.Add('SmoothMetricRecords', 'count', len(interaction_records))
# [ X ]
# [ Y ]
renderer_thread.BeginSlice('cat1', 'x.y', 10, 0)
renderer_thread.EndSlice(20, 20)
renderer_thread.async_slices.append(async_slice.AsyncSlice(
'cat', 'Interaction.LogicalName1/is_smooth',
timestamp=0, duration=20,
start_thread=renderer_thread, end_thread=renderer_thread,
thread_start=5, thread_duration=15))
renderer_thread.async_slices.append(async_slice.AsyncSlice(
'cat', 'Interaction.LogicalName2/is_responsive',
timestamp=25, duration=5,
start_thread=renderer_thread, end_thread=renderer_thread,
thread_start=25, thread_duration=5))
model.FinalizeImport()
self.model = model
self.renderer_thread = renderer_thread
def testFindTimelineInteractionRecords(self):
metric = tbm_module._TimelineBasedMetrics( # pylint: disable=W0212
self.model, self.renderer_thread, lambda _: [])
interactions = metric.FindTimelineInteractionRecords()
self.assertEquals(2, len(interactions))
self.assertTrue(interactions[0].is_smooth)
self.assertEquals(0, interactions[0].start)
self.assertEquals(20, interactions[0].end)
self.assertTrue(interactions[1].is_responsive) class FakeLoadingMetric(timeline_based_metric.TimelineBasedMetric):
self.assertEquals(25, interactions[1].start)
self.assertEquals(30, interactions[1].end)
def testAddResults(self): def AddResults(self, model, renderer_thread,
results = page_measurement_results.PageMeasurementResults() interaction_records, results):
results.Add('FakeLoadingMetric', 'ms', 2)
results.Add('LoadingMetricRecords', 'count', len(interaction_records))
def GetMetricFromMetricType(metric_type):
if metric_type == tir_module.IS_SMOOTH:
return FakeSmoothMetric()
if metric_type == tir_module.IS_RESPONSIVE:
return FakeLoadingMetric()
raise Exception('Unrecognized metric type: %s' % metric_type)
class FakeSmoothMetric(timeline_based_metric.TimelineBasedMetric):
def AddResults(self, model, renderer_thread, class TimelineBasedMetricTestData(object):
interaction_records, results):
results.Add('FakeSmoothMetric', 'ms', 1)
class FakeLoadingMetric(timeline_based_metric.TimelineBasedMetric): def __init__(self):
self._model = model_module.TimelineModel()
renderer_process = self._model.GetOrCreateProcess(1)
self._renderer_thread = renderer_process.GetOrCreateThread(2)
self._renderer_thread.name = 'CrRendererMain'
self._results = page_measurement_results.PageMeasurementResults()
self._metric = None
def AddResults(self, model, renderer_thread, @property
interaction_records, results): def results(self):
for r in interaction_records: return self._results
assert r.logical_name == 'LogicalName2'
results.Add('FakeLoadingMetric', 'ms', 2)
def CreateMetricsForTimelineInteractionRecord(interaction): @property
res = [] def metric(self):
if interaction.is_smooth: return self._metric
res.append(FakeSmoothMetric())
if interaction.is_responsive:
res.append(FakeLoadingMetric())
return res
metric = tbm_module._TimelineBasedMetrics( # pylint: disable=W0212 def AddInteraction(self, marker='', ts=0, duration=5):
self.model, self.renderer_thread, self._renderer_thread.async_slices.append(async_slice.AsyncSlice(
CreateMetricsForTimelineInteractionRecord) 'category', marker, timestamp=ts, duration=duration,
start_thread=self._renderer_thread, end_thread=self._renderer_thread,
thread_start=ts, thread_duration=duration))
def FinalizeImport(self):
self._model.FinalizeImport()
self._metric = tbm_module._TimelineBasedMetrics( # pylint: disable=W0212
self._model, self._renderer_thread, GetMetricFromMetricType)
ps = page_set.PageSet(file_path=os.path.dirname(__file__)) ps = page_set.PageSet(file_path=os.path.dirname(__file__))
ps.AddPageWithDefaultRunNavigate('http://www.bar.com/') ps.AddPageWithDefaultRunNavigate('http://www.bar.com/')
self._results.WillMeasurePage(ps.pages[0])
results.WillMeasurePage(ps.pages[0]) def AddResults(self):
metric.AddResults(results) self._metric.AddResults(self._results)
results.DidMeasurePage() self._results.DidMeasurePage()
v = results.FindAllPageSpecificValuesNamed('LogicalName1-FakeSmoothMetric')
self.assertEquals(len(v), 1) class TimelineBasedMetricsTests(unittest.TestCase):
v = results.FindAllPageSpecificValuesNamed('LogicalName2-FakeLoadingMetric')
self.assertEquals(len(v), 1) def testFindTimelineInteractionRecords(self):
d = TimelineBasedMetricTestData()
d.AddInteraction(ts=0, duration=20,
marker='Interaction.LogicalName1/is_smooth')
d.AddInteraction(ts=25, duration=5,
marker='Interaction.LogicalName2/is_responsive')
d.FinalizeImport()
interactions = d.metric.FindTimelineInteractionRecords()
self.assertEquals(2, len(interactions))
self.assertTrue(interactions[0].is_smooth)
self.assertEquals(0, interactions[0].start)
self.assertEquals(20, interactions[0].end)
self.assertTrue(interactions[1].is_responsive)
self.assertEquals(25, interactions[1].start)
self.assertEquals(30, interactions[1].end)
def testAddResults(self):
d = TimelineBasedMetricTestData()
d.AddInteraction(ts=0, duration=20,
marker='Interaction.LogicalName1/is_smooth')
d.AddInteraction(ts=25, duration=5,
marker='Interaction.LogicalName2/is_responsive')
d.FinalizeImport()
d.AddResults()
self.assertEquals(1, len(d.results.FindAllPageSpecificValuesNamed(
'LogicalName1-FakeSmoothMetric')))
self.assertEquals(1, len(d.results.FindAllPageSpecificValuesNamed(
'LogicalName2-FakeLoadingMetric')))
def testNoInteractions(self):
d = TimelineBasedMetricTestData()
d.FinalizeImport()
self.assertRaises(tbm_module.InvalidInteractions, d.AddResults)
def testDuplicateUnrepeatableInteractions(self):
d = TimelineBasedMetricTestData()
d.AddInteraction(ts=10, duration=5,
marker='Interaction.LogicalName/is_smooth')
d.AddInteraction(ts=20, duration=5,
marker='Interaction.LogicalName/is_smooth')
d.FinalizeImport()
self.assertRaises(tbm_module.InvalidInteractions, d.AddResults)
def testDuplicateRepeatableInteractions(self):
d = TimelineBasedMetricTestData()
d.AddInteraction(ts=10, duration=5,
marker='Interaction.LogicalName/is_smooth,repeatable')
d.AddInteraction(ts=20, duration=5,
marker='Interaction.LogicalName/is_smooth,repeatable')
d.FinalizeImport()
d.AddResults()
self.assertEquals(1, len(d.results.pages_that_succeeded))
def testDuplicateRepeatableInteractionsWithDifferentMetrics(self):
d = TimelineBasedMetricTestData()
responsive_marker = 'Interaction.LogicalName/is_responsive,repeatable'
d.AddInteraction(ts=10, duration=5, marker=responsive_marker)
smooth_marker = 'Interaction.LogicalName/is_smooth,repeatable'
d.AddInteraction(ts=20, duration=5, marker=smooth_marker)
d.FinalizeImport()
self.assertRaises(tbm_module.InvalidInteractions, d.AddResults)
class TestTimelinebasedMeasurementPage(page_module.Page): class TestTimelinebasedMeasurementPage(page_module.Page):
......
...@@ -8,13 +8,18 @@ from telemetry import decorators ...@@ -8,13 +8,18 @@ from telemetry import decorators
import telemetry.timeline.bounds as timeline_bounds import telemetry.timeline.bounds as timeline_bounds
# Enables the smoothness metric for this interaction
IS_SMOOTH = 'is_smooth' IS_SMOOTH = 'is_smooth'
# Enables the responsiveness metric for this interaction
IS_RESPONSIVE = 'is_responsive' IS_RESPONSIVE = 'is_responsive'
# Allows multiple duplicate interactions of the same type
REPEATABLE = 'repeatable'
FLAGS = [ METRICS = [
IS_SMOOTH, IS_RESPONSIVE,
IS_RESPONSIVE IS_SMOOTH
] ]
FLAGS = METRICS + [REPEATABLE]
class ThreadTimeRangeOverlappedException(Exception): class ThreadTimeRangeOverlappedException(Exception):
...@@ -29,6 +34,12 @@ class NoThreadTimeDataException(ThreadTimeRangeOverlappedException): ...@@ -29,6 +34,12 @@ class NoThreadTimeDataException(ThreadTimeRangeOverlappedException):
def IsTimelineInteractionRecord(event_name): def IsTimelineInteractionRecord(event_name):
return event_name.startswith('Interaction.') return event_name.startswith('Interaction.')
def _AssertFlagsAreValid(flags):
assert isinstance(flags, list)
for f in flags:
if f not in FLAGS:
raise AssertionError(
'Unrecognized flag for a timeline Interaction record: %s' % f)
class TimelineInteractionRecord(object): class TimelineInteractionRecord(object):
"""Represents an interaction that took place during a timeline recording. """Represents an interaction that took place during a timeline recording.
...@@ -66,6 +77,10 @@ class TimelineInteractionRecord(object): ...@@ -66,6 +77,10 @@ class TimelineInteractionRecord(object):
smoothness and network metrics to be reported for the marked up 1000ms smoothness and network metrics to be reported for the marked up 1000ms
time-range. time-range.
The valid interaction flags are:
* is_smooth: Enables the smoothness metrics
* is_responsive: Enables the responsiveness metrics
* repeatable: Allows other interactions to use the same logical name
""" """
def __init__(self, logical_name, start, end, async_event=None): def __init__(self, logical_name, start, end, async_event=None):
...@@ -75,6 +90,7 @@ class TimelineInteractionRecord(object): ...@@ -75,6 +90,7 @@ class TimelineInteractionRecord(object):
self.end = end self.end = end
self.is_smooth = False self.is_smooth = False
self.is_responsive = False self.is_responsive = False
self.repeatable = False
self._async_event = async_event self._async_event = async_event
# TODO(nednguyen): After crbug.com/367175 is marked fixed, we should be able # TODO(nednguyen): After crbug.com/367175 is marked fixed, we should be able
...@@ -105,17 +121,12 @@ class TimelineInteractionRecord(object): ...@@ -105,17 +121,12 @@ class TimelineInteractionRecord(object):
record = TimelineInteractionRecord(logical_name, async_event.start, record = TimelineInteractionRecord(logical_name, async_event.start,
async_event.end, async_event) async_event.end, async_event)
for f in flags: _AssertFlagsAreValid(flags)
if not f in FLAGS:
raise Exception(
'Unrecognized flag in timeline Interaction record: %s' % f)
record.is_smooth = IS_SMOOTH in flags record.is_smooth = IS_SMOOTH in flags
record.is_responsive = IS_RESPONSIVE in flags record.is_responsive = IS_RESPONSIVE in flags
record.repeatable = REPEATABLE in flags
return record return record
def GetResultNameFor(self, result_name):
return '%s-%s' % (self.logical_name, result_name)
@decorators.Cache @decorators.Cache
def GetBounds(self): def GetBounds(self):
bounds = timeline_bounds.Bounds() bounds = timeline_bounds.Bounds()
...@@ -128,13 +139,15 @@ class TimelineInteractionRecord(object): ...@@ -128,13 +139,15 @@ class TimelineInteractionRecord(object):
""" Get the marker string of an interaction record with logical_name """ Get the marker string of an interaction record with logical_name
and flags. and flags.
""" """
assert isinstance(flags, list) _AssertFlagsAreValid(flags)
for f in flags:
if f not in FLAGS:
raise AssertionError(
'Unrecognized flag for a timeline Interaction record: %s' % f)
return 'Interaction.%s/%s' % (logical_name, ','.join(flags)) return 'Interaction.%s/%s' % (logical_name, ','.join(flags))
def HasMetric(self, metric_type):
if metric_type not in METRICS:
raise AssertionError('Unrecognized metric type for a timeline '
'interaction record: %s' % metric_type)
return getattr(self, metric_type)
def GetOverlappedThreadTimeForSlice(self, timeline_slice): def GetOverlappedThreadTimeForSlice(self, timeline_slice):
"""Get the thread duration of timeline_slice that overlaps with this record. """Get the thread duration of timeline_slice that overlaps with this record.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment