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.
4
5import re
6
7import telemetry.timeline.bounds as timeline_bounds
8from telemetry import decorators
9
10# Enables the fast metric for this interaction
11IS_FAST = 'is_fast'
12# Enables the responsiveness metric for this interaction
13IS_RESPONSIVE = 'is_responsive'
14# Enables the smoothness metric for this interaction
15IS_SMOOTH = 'is_smooth'
16# Allows multiple duplicate interactions of the same type
17REPEATABLE = 'repeatable'
18
19METRICS = [
20    IS_FAST,
21    IS_RESPONSIVE,
22    IS_SMOOTH
23]
24FLAGS = METRICS + [REPEATABLE]
25
26
27class ThreadTimeRangeOverlappedException(Exception):
28  """Exception that can be thrown when computing overlapped thread time range
29  with other events.
30  """
31
32class NoThreadTimeDataException(ThreadTimeRangeOverlappedException):
33  """Exception that can be thrown if there is not sufficient thread time data
34  to compute the overlapped thread time range."""
35
36def IsTimelineInteractionRecord(event_name):
37  return event_name.startswith('Interaction.')
38
39def _AssertFlagsAreValid(flags):
40  assert isinstance(flags, list)
41  for f in flags:
42    if f not in FLAGS:
43      raise AssertionError(
44          'Unrecognized flag for a timeline interaction record: %s' % f)
45
46def GetJavaScriptMarker(label, flags):
47  """Computes the marker string of an interaction record.
48
49  This marker string can be used with JavaScript API console.time()
50  and console.timeEnd() to mark the beginning and end of the
51  interaction record..
52
53  Args:
54    label: The label used to identify the interaction record.
55    flags: the flags for the interaction record see FLAGS above.
56
57  Returns:
58    The interaction record marker string (e.g., Interaction.Label/flag1,flag2).
59
60  Raises:
61    AssertionError: If one or more of the flags is unrecognized.
62  """
63  _AssertFlagsAreValid(flags)
64  return 'Interaction.%s/%s' % (label, ','.join(flags))
65
66class TimelineInteractionRecord(object):
67  """Represents an interaction that took place during a timeline recording.
68
69  As a page runs, typically a number of different (simulated) user interactions
70  take place. For instance, a user might click a button in a mail app causing a
71  popup to animate in. Then they might press another button that sends data to a
72  server and simultaneously closes the popup without an animation. These are two
73  interactions.
74
75  From the point of view of the page, each interaction might have a different
76  label: ClickComposeButton and SendEmail, for instance. From the point
77  of view of the benchmarking harness, the labels aren't so interesting as what
78  the performance expectations are for that interaction: was it loading
79  resources from the network? was there an animation?
80
81  Determining these things is hard to do, simply by observing the state given to
82  a page from javascript. There are hints, for instance if network requests are
83  sent, or if a CSS animation is pending. But this is by no means a complete
84  story.
85
86  Instead, we expect pages to mark up the timeline what they are doing, with
87  label and flags indicating the semantics of that interaction. This
88  is currently done by pushing markers into the console.time/timeEnd API: this
89  for instance can be issued in JS:
90
91     var str = 'Interaction.SendEmail/is_smooth,is_responsive,is_fast';
92     console.time(str);
93     setTimeout(function() {
94       console.timeEnd(str);
95     }, 1000);
96
97  When run with perf.measurements.timeline_based_measurement running, this will
98  then cause a TimelineInteractionRecord to be created for this range with
99  smoothness, responsive, and fast metrics reported for the marked up 1000ms
100  time-range.
101
102  The valid interaction flags are:
103     * is_fast: Enables the fast metric
104     * is_responsive: Enables the responsiveness metric
105     * is_smooth: Enables the smoothness metric
106     * repeatable: Allows other interactions to use the same label
107  """
108
109  def __init__(self, label, start, end, async_event=None, flags=None):
110    assert label
111    self._label = label
112    self._start = start
113    self._end = end
114    self._async_event = async_event
115    self._flags = flags if flags is not None else []
116    _AssertFlagsAreValid(self._flags)
117
118  @property
119  def label(self):
120    return self._label
121
122  @property
123  def start(self):
124    return self._start
125
126  @property
127  def end(self):
128    return self._end
129
130  @property
131  def is_fast(self):
132    return IS_FAST in self._flags
133
134  @property
135  def is_responsive(self):
136    return IS_RESPONSIVE in self._flags
137
138  @property
139  def is_smooth(self):
140    return IS_SMOOTH in self._flags
141
142  @property
143  def repeatable(self):
144    return REPEATABLE in self._flags
145
146  # TODO(nednguyen): After crbug.com/367175 is marked fixed, we should be able
147  # to get rid of perf.measurements.smooth_gesture_util and make this the only
148  # constructor method for TimelineInteractionRecord.
149  @classmethod
150  def FromAsyncEvent(cls, async_event):
151    """Construct an timeline_interaction_record from an async event.
152    Args:
153      async_event: An instance of
154        telemetry.timeline.async_slices.AsyncSlice
155    """
156    assert async_event.start_thread == async_event.end_thread, (
157        'Start thread of this record\'s async event is not the same as its '
158        'end thread')
159    m = re.match('Interaction\.(?P<label>.+?)(/(?P<flags>[^/]+))?$',
160                 async_event.name)
161    assert m, "Async event is not an interaction record."
162    label = m.group('label')
163    flags = m.group('flags').split(',') if m.group('flags') is not None else []
164    return cls(label, async_event.start, async_event.end, async_event, flags)
165
166  @decorators.Cache
167  def GetBounds(self):
168    bounds = timeline_bounds.Bounds()
169    bounds.AddValue(self.start)
170    bounds.AddValue(self.end)
171    return bounds
172
173  def HasMetric(self, metric_type):
174    if metric_type not in METRICS:
175      raise AssertionError('Unrecognized metric type for a timeline '
176                           'interaction record: %s' % metric_type)
177    return metric_type in self._flags
178
179  def GetOverlappedThreadTimeForSlice(self, timeline_slice):
180    """Get the thread duration of timeline_slice that overlaps with this record.
181
182    There are two cases :
183
184    Case 1: timeline_slice runs in the same thread as the record.
185
186                  |    [       timeline_slice         ]
187      THREAD 1    |                  |                              |
188                  |            record starts                    record ends
189
190                      (relative order in thread time)
191
192      As the thread timestamps in timeline_slice and record are consistent, we
193      simply use them to compute the overlap.
194
195    Case 2: timeline_slice runs in a different thread from the record's.
196
197                  |
198      THREAD 2    |    [       timeline_slice         ]
199                  |
200
201                  |
202      THREAD 1    |               |                               |
203                  |          record starts                      record ends
204
205                      (relative order in wall-time)
206
207      Unlike case 1, thread timestamps of a thread are measured by its
208      thread-specific clock, which is inconsistent with that of the other
209      thread, and thus can't be used to compute the overlapped thread duration.
210      Hence, we use a heuristic to compute the overlap (see
211      _GetOverlappedThreadTimeForSliceInDifferentThread for more details)
212
213    Args:
214      timeline_slice: An instance of telemetry.timeline.slice.Slice
215    """
216    if not self._async_event:
217      raise ThreadTimeRangeOverlappedException(
218          'This record was not constructed from async event')
219    if not self._async_event.has_thread_timestamps:
220      raise NoThreadTimeDataException(
221          'This record\'s async_event does not contain thread time data. '
222          'Event data: %s' % repr(self._async_event))
223    if not timeline_slice.has_thread_timestamps:
224      raise NoThreadTimeDataException(
225          'slice does not contain thread time data')
226
227    if timeline_slice.parent_thread == self._async_event.start_thread:
228      return self._GetOverlappedThreadTimeForSliceInSameThread(
229          timeline_slice)
230    else:
231      return self._GetOverlappedThreadTimeForSliceInDifferentThread(
232          timeline_slice)
233
234  def _GetOverlappedThreadTimeForSliceInSameThread(self, timeline_slice):
235    return timeline_bounds.Bounds.GetOverlap(
236        timeline_slice.thread_start, timeline_slice.thread_end,
237        self._async_event.thread_start, self._async_event.thread_end)
238
239  def _GetOverlappedThreadTimeForSliceInDifferentThread(self, timeline_slice):
240    # In case timeline_slice's parent thread is not the parent thread of the
241    # async slice that issues this record, we assume that events are descheduled
242    # uniformly. The overlap duration in thread time is then computed by
243    # multiplying the overlap wall-time duration of timeline_slice and the
244    # record's async slice with their thread_duration/duration ratios.
245    overlapped_walltime_duration = timeline_bounds.Bounds.GetOverlap(
246        timeline_slice.start, timeline_slice.end,
247        self.start, self.end)
248    if timeline_slice.duration == 0 or self._async_event.duration == 0:
249      return 0
250    timeline_slice_scheduled_ratio = (
251        timeline_slice.thread_duration / float(timeline_slice.duration))
252    record_scheduled_ratio = (
253        self._async_event.thread_duration / float(self._async_event.duration))
254    return (overlapped_walltime_duration * timeline_slice_scheduled_ratio *
255            record_scheduled_ratio)
256
257  def __repr__(self):
258    flags_str = ','.join(self._flags)
259    return ('TimelineInteractionRecord(label=\'%s\', start=%f, end=%f,' +
260            ' flags=%s, async_event=%s)') % (
261                self.label,
262                self.start,
263                self.end,
264                flags_str,
265                repr(self._async_event))
266