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.
8 import telemetry.timeline.async_slice as tracing_async_slice
9 import telemetry.timeline.bounds as timeline_bounds
10 from telemetry.perf_tests_helper import FlattenList
11 from telemetry.timeline import model
12 from telemetry.util.statistics import DivideIfPossibleOrZero
13 from telemetry.web_perf.metrics.rendering_stats import (
15 BEGIN_SCROLL_UPDATE_COMP_NAME,
17 FORWARD_SCROLL_UPDATE_COMP_NAME,
18 GESTURE_SCROLL_UPDATE_EVENT_NAME,
20 SCROLL_UPDATE_EVENT_NAME,
22 from telemetry.web_perf.metrics.rendering_stats import (
23 ComputeInputEventLatencies)
24 from telemetry.web_perf.metrics.rendering_stats import GetInputLatencyEvents
25 from telemetry.web_perf.metrics.rendering_stats import HasRenderingStats
26 from telemetry.web_perf.metrics.rendering_stats import RenderingStats
29 class MockTimer(object):
30 """A mock timer class which can generate random durations.
32 An instance of this class is used as a global timer to generate random
33 durations for stats and consistent timestamps for all mock trace events.
34 The unit of time is milliseconds.
40 return self.milliseconds
42 def Advance(self, low=0, high=1):
43 delta = random.uniform(low, high)
44 self.milliseconds += delta
48 class ReferenceRenderingStats(object):
49 """ Stores expected data for comparison with actual RenderingStats """
51 self.frame_timestamps = []
54 self.painted_pixel_counts = []
55 self.record_times = []
56 self.recorded_pixel_counts = []
57 self.approximated_pixel_percentages = []
59 def AppendNewRange(self):
60 self.frame_timestamps.append([])
61 self.frame_times.append([])
62 self.paint_times.append([])
63 self.painted_pixel_counts.append([])
64 self.record_times.append([])
65 self.recorded_pixel_counts.append([])
66 self.approximated_pixel_percentages.append([])
68 class ReferenceInputLatencyStats(object):
69 """ Stores expected data for comparison with actual input latency stats """
71 self.input_event_latency = []
74 def AddMainThreadRenderingStats(mock_timer, thread, ref_stats = None):
75 """ Adds a random main thread rendering stats event.
77 thread: The timeline model thread to which the event will be added.
78 first_frame: Is this the first frame within the bounds of an action?
79 ref_stats: A ReferenceRenderingStats object to record expected values.
81 # Create randonm data and timestap for main thread rendering stats.
82 data = { 'paint_time': 0.0,
83 'painted_pixel_count': 0,
84 'record_time': mock_timer.Advance(2, 4) / 1000.0,
85 'recorded_pixel_count': 3000*3000 }
86 timestamp = mock_timer.Get()
88 # Add a slice with the event data to the given thread.
89 thread.PushCompleteSlice(
90 'benchmark', 'BenchmarkInstrumentation::MainThreadRenderingStats',
91 timestamp, duration=0.0, thread_timestamp=None, thread_duration=None,
97 ref_stats.paint_times[-1].append(data['paint_time'] * 1000.0)
98 ref_stats.painted_pixel_counts[-1].append(data['painted_pixel_count'])
99 ref_stats.record_times[-1].append(data['record_time'] * 1000.0)
100 ref_stats.recorded_pixel_counts[-1].append(data['recorded_pixel_count'])
103 def AddDisplayRenderingStats(mock_timer, thread, first_frame,
105 """ Adds a random display rendering stats event.
107 thread: The timeline model thread to which the event will be added.
108 first_frame: Is this the first frame within the bounds of an action?
109 ref_stats: A ReferenceRenderingStats object to record expected values.
111 # Create randonm data and timestap for main thread rendering stats.
112 data = { 'frame_count': 1 }
113 timestamp = mock_timer.Get()
115 # Add a slice with the event data to the given thread.
116 thread.PushCompleteSlice(
117 'benchmark', 'BenchmarkInstrumentation::DisplayRenderingStats',
118 timestamp, duration=0.0, thread_timestamp=None, thread_duration=None,
124 # Add timestamp only if a frame was output
126 # Add frame_time if this is not the first frame in within the bounds of an
128 prev_timestamp = ref_stats.frame_timestamps[-1][-1]
129 ref_stats.frame_times[-1].append(round(timestamp - prev_timestamp, 2))
130 ref_stats.frame_timestamps[-1].append(timestamp)
133 def AddImplThreadRenderingStats(mock_timer, thread, first_frame,
135 """ Adds a random impl thread rendering stats event.
137 thread: The timeline model thread to which the event will be added.
138 first_frame: Is this the first frame within the bounds of an action?
139 ref_stats: A ReferenceRenderingStats object to record expected values.
141 # Create randonm data and timestap for impl thread rendering stats.
142 data = { 'frame_count': 1,
143 'visible_content_area': random.uniform(0, 100),
144 'approximated_visible_content_area': random.uniform(0, 5)}
145 timestamp = mock_timer.Get()
147 # Add a slice with the event data to the given thread.
148 thread.PushCompleteSlice(
149 'benchmark', 'BenchmarkInstrumentation::ImplThreadRenderingStats',
150 timestamp, duration=0.0, thread_timestamp=None, thread_duration=None,
156 # Add timestamp only if a frame was output
157 if data['frame_count'] == 1:
159 # Add frame_time if this is not the first frame in within the bounds of an
161 prev_timestamp = ref_stats.frame_timestamps[-1][-1]
162 ref_stats.frame_times[-1].append(round(timestamp - prev_timestamp, 2))
163 ref_stats.frame_timestamps[-1].append(timestamp)
165 ref_stats.approximated_pixel_percentages[-1].append(
166 round(DivideIfPossibleOrZero(data['approximated_visible_content_area'],
167 data['visible_content_area']) * 100.0, 3))
170 def AddInputLatencyStats(mock_timer, start_thread, end_thread,
171 ref_latency_stats = None):
172 """ Adds a random input latency stats event.
174 start_thread: The start thread on which the async slice is added.
175 end_thread: The end thread on which the async slice is ended.
176 ref_latency_stats: A ReferenceInputLatencyStats object for expected values.
179 mock_timer.Advance(2, 4)
180 original_comp_time = mock_timer.Get() * 1000.0
181 mock_timer.Advance(2, 4)
182 ui_comp_time = mock_timer.Get() * 1000.0
183 mock_timer.Advance(2, 4)
184 begin_comp_time = mock_timer.Get() * 1000.0
185 mock_timer.Advance(2, 4)
186 forward_comp_time = mock_timer.Get() * 1000.0
187 mock_timer.Advance(10, 20)
188 end_comp_time = mock_timer.Get() * 1000.0
190 data = { ORIGINAL_COMP_NAME: {'time': original_comp_time},
191 UI_COMP_NAME: {'time': ui_comp_time},
192 BEGIN_COMP_NAME: {'time': begin_comp_time},
193 END_COMP_NAME: {'time': end_comp_time} }
195 timestamp = mock_timer.Get()
197 async_slice = tracing_async_slice.AsyncSlice(
198 'benchmark', 'InputLatency', timestamp)
200 async_sub_slice = tracing_async_slice.AsyncSlice(
201 'benchmark', GESTURE_SCROLL_UPDATE_EVENT_NAME, timestamp)
202 async_sub_slice.args = {'data': data}
203 async_sub_slice.parent_slice = async_slice
204 async_sub_slice.start_thread = start_thread
205 async_sub_slice.end_thread = end_thread
207 async_slice.sub_slices.append(async_sub_slice)
208 async_slice.start_thread = start_thread
209 async_slice.end_thread = end_thread
210 start_thread.AddAsyncSlice(async_slice)
212 # Add scroll update latency info.
213 scroll_update_data = {
214 BEGIN_SCROLL_UPDATE_COMP_NAME: {'time': begin_comp_time},
215 FORWARD_SCROLL_UPDATE_COMP_NAME: {'time': forward_comp_time},
216 END_COMP_NAME: {'time': end_comp_time} }
218 scroll_async_slice = tracing_async_slice.AsyncSlice(
219 'benchmark', 'InputLatency', timestamp)
221 scroll_async_sub_slice = tracing_async_slice.AsyncSlice(
222 'benchmark', SCROLL_UPDATE_EVENT_NAME, timestamp)
223 scroll_async_sub_slice.args = {'data': scroll_update_data}
224 scroll_async_sub_slice.parent_slice = scroll_async_slice
225 scroll_async_sub_slice.start_thread = start_thread
226 scroll_async_sub_slice.end_thread = end_thread
228 scroll_async_slice.sub_slices.append(scroll_async_sub_slice)
229 scroll_async_slice.start_thread = start_thread
230 scroll_async_slice.end_thread = end_thread
231 start_thread.AddAsyncSlice(scroll_async_slice)
233 # Also add some dummy frame statistics so we can feed the resulting timeline
235 AddMainThreadRenderingStats(mock_timer, start_thread)
236 AddImplThreadRenderingStats(mock_timer, end_thread, False)
238 if not ref_latency_stats:
241 ref_latency_stats.input_event.append(async_sub_slice)
242 ref_latency_stats.input_event.append(scroll_async_sub_slice)
243 ref_latency_stats.input_event_latency.append((
244 GESTURE_SCROLL_UPDATE_EVENT_NAME,
245 (data[END_COMP_NAME]['time'] -
246 data[ORIGINAL_COMP_NAME]['time']) / 1000.0))
247 ref_latency_stats.input_event_latency.append((
248 SCROLL_UPDATE_EVENT_NAME,
249 (scroll_update_data[END_COMP_NAME]['time'] -
250 scroll_update_data[BEGIN_SCROLL_UPDATE_COMP_NAME]['time']) / 1000.0))
253 class RenderingStatsUnitTest(unittest.TestCase):
254 def testHasRenderingStats(self):
255 timeline = model.TimelineModel()
258 # A process without rendering stats
259 process_without_stats = timeline.GetOrCreateProcess(pid = 1)
260 thread_without_stats = process_without_stats.GetOrCreateThread(tid = 11)
261 process_without_stats.FinalizeImport()
262 self.assertFalse(HasRenderingStats(thread_without_stats))
264 # A process with rendering stats, but no frames in them
265 process_without_frames = timeline.GetOrCreateProcess(pid = 2)
266 thread_without_frames = process_without_frames.GetOrCreateThread(tid = 21)
267 AddMainThreadRenderingStats(timer, thread_without_frames, None)
268 process_without_frames.FinalizeImport()
269 self.assertFalse(HasRenderingStats(thread_without_frames))
271 # A process with rendering stats and frames in them
272 process_with_frames = timeline.GetOrCreateProcess(pid = 3)
273 thread_with_frames = process_with_frames.GetOrCreateThread(tid = 31)
274 AddImplThreadRenderingStats(timer, thread_with_frames, True, None)
275 process_with_frames.FinalizeImport()
276 self.assertTrue(HasRenderingStats(thread_with_frames))
278 def testBothDisplayAndImplStats(self):
279 timeline = model.TimelineModel()
282 ref_stats = ReferenceRenderingStats()
283 ref_stats.AppendNewRange()
284 renderer = timeline.GetOrCreateProcess(pid = 2)
285 browser = timeline.GetOrCreateProcess(pid = 3)
286 browser_main = browser.GetOrCreateThread(tid = 31)
287 browser_main.BeginSlice('webkit.console', 'ActionA', timer.Get(), '')
289 # Create main, impl, and display rendering stats.
290 for i in xrange(0, 10):
292 AddMainThreadRenderingStats(timer, browser_main, ref_stats)
293 AddImplThreadRenderingStats(timer, browser_main, first, None)
296 for i in xrange(0, 10):
298 AddDisplayRenderingStats(timer, browser_main, first, ref_stats)
301 browser_main.EndSlice(timer.Get())
303 browser.FinalizeImport()
304 renderer.FinalizeImport()
305 timeline_markers = timeline.FindTimelineMarkers(['ActionA'])
306 timeline_ranges = [ timeline_bounds.Bounds.CreateFromEvent(marker)
307 for marker in timeline_markers ]
308 stats = RenderingStats(renderer, browser, timeline_ranges)
310 # Compare rendering stats to reference - Only display stats should count
311 self.assertEquals(stats.frame_timestamps, ref_stats.frame_timestamps)
312 self.assertEquals(stats.frame_times, ref_stats.frame_times)
314 def testRangeWithoutFrames(self):
316 timeline = model.TimelineModel()
318 # Create a renderer process, with a main thread and impl thread.
319 renderer = timeline.GetOrCreateProcess(pid = 2)
320 renderer_main = renderer.GetOrCreateThread(tid = 21)
321 renderer_compositor = renderer.GetOrCreateThread(tid = 22)
323 # Create 10 main and impl rendering stats events for Action A.
325 renderer_main.BeginSlice('webkit.console', 'ActionA', timer.Get(), '')
326 for i in xrange(0, 10):
328 AddMainThreadRenderingStats(timer, renderer_main, None)
329 AddImplThreadRenderingStats(timer, renderer_compositor, first, None)
331 renderer_main.EndSlice(timer.Get())
333 # Create 5 main and impl rendering stats events not within any action.
334 for i in xrange(0, 5):
336 AddMainThreadRenderingStats(timer, renderer_main, None)
337 AddImplThreadRenderingStats(timer, renderer_compositor, first, None)
339 # Create Action B without any frames. This should trigger
340 # NotEnoughFramesError when the RenderingStats object is created.
342 renderer_main.BeginSlice('webkit.console', 'ActionB', timer.Get(), '')
344 renderer_main.EndSlice(timer.Get())
346 renderer.FinalizeImport()
348 timeline_markers = timeline.FindTimelineMarkers(['ActionA', 'ActionB'])
349 timeline_ranges = [ timeline_bounds.Bounds.CreateFromEvent(marker)
350 for marker in timeline_markers ]
352 stats = RenderingStats(renderer, None, timeline_ranges)
353 self.assertEquals(0, len(stats.frame_timestamps[1]))
356 def testFromTimeline(self):
357 timeline = model.TimelineModel()
359 # Create a browser process and a renderer process, and a main thread and
360 # impl thread for each.
361 browser = timeline.GetOrCreateProcess(pid = 1)
362 browser_main = browser.GetOrCreateThread(tid = 11)
363 browser_compositor = browser.GetOrCreateThread(tid = 12)
364 renderer = timeline.GetOrCreateProcess(pid = 2)
365 renderer_main = renderer.GetOrCreateThread(tid = 21)
366 renderer_compositor = renderer.GetOrCreateThread(tid = 22)
369 renderer_ref_stats = ReferenceRenderingStats()
370 browser_ref_stats = ReferenceRenderingStats()
372 # Create 10 main and impl rendering stats events for Action A.
374 renderer_main.BeginSlice('webkit.console', 'ActionA', timer.Get(), '')
375 renderer_ref_stats.AppendNewRange()
376 browser_ref_stats.AppendNewRange()
377 for i in xrange(0, 10):
379 AddMainThreadRenderingStats(
380 timer, renderer_main, renderer_ref_stats)
381 AddImplThreadRenderingStats(
382 timer, renderer_compositor, first, renderer_ref_stats)
383 AddMainThreadRenderingStats(
384 timer, browser_main, browser_ref_stats)
385 AddImplThreadRenderingStats(
386 timer, browser_compositor, first, browser_ref_stats)
388 renderer_main.EndSlice(timer.Get())
390 # Create 5 main and impl rendering stats events not within any action.
391 for i in xrange(0, 5):
393 AddMainThreadRenderingStats(timer, renderer_main, None)
394 AddImplThreadRenderingStats(timer, renderer_compositor, first, None)
395 AddMainThreadRenderingStats(timer, browser_main, None)
396 AddImplThreadRenderingStats(timer, browser_compositor, first, None)
398 # Create 10 main and impl rendering stats events for Action B.
400 renderer_main.BeginSlice('webkit.console', 'ActionB', timer.Get(), '')
401 renderer_ref_stats.AppendNewRange()
402 browser_ref_stats.AppendNewRange()
403 for i in xrange(0, 10):
405 AddMainThreadRenderingStats(
406 timer, renderer_main, renderer_ref_stats)
407 AddImplThreadRenderingStats(
408 timer, renderer_compositor, first, renderer_ref_stats)
409 AddMainThreadRenderingStats(
410 timer, browser_main, browser_ref_stats)
411 AddImplThreadRenderingStats(
412 timer, browser_compositor, first, browser_ref_stats)
414 renderer_main.EndSlice(timer.Get())
416 # Create 10 main and impl rendering stats events for Action A.
418 renderer_main.BeginSlice('webkit.console', 'ActionA', timer.Get(), '')
419 renderer_ref_stats.AppendNewRange()
420 browser_ref_stats.AppendNewRange()
421 for i in xrange(0, 10):
423 AddMainThreadRenderingStats(
424 timer, renderer_main, renderer_ref_stats)
425 AddImplThreadRenderingStats(
426 timer, renderer_compositor, first, renderer_ref_stats)
427 AddMainThreadRenderingStats(
428 timer, browser_main, browser_ref_stats)
429 AddImplThreadRenderingStats(
430 timer, browser_compositor, first, browser_ref_stats)
432 renderer_main.EndSlice(timer.Get())
434 browser.FinalizeImport()
435 renderer.FinalizeImport()
437 timeline_markers = timeline.FindTimelineMarkers(
438 ['ActionA', 'ActionB', 'ActionA'])
439 timeline_ranges = [ timeline_bounds.Bounds.CreateFromEvent(marker)
440 for marker in timeline_markers ]
441 stats = RenderingStats(renderer, browser, timeline_ranges)
443 # Compare rendering stats to reference.
444 self.assertEquals(stats.frame_timestamps,
445 browser_ref_stats.frame_timestamps)
446 self.assertEquals(stats.frame_times, browser_ref_stats.frame_times)
447 self.assertEquals(stats.approximated_pixel_percentages,
448 renderer_ref_stats.approximated_pixel_percentages)
449 self.assertEquals(stats.paint_times, renderer_ref_stats.paint_times)
450 self.assertEquals(stats.painted_pixel_counts,
451 renderer_ref_stats.painted_pixel_counts)
452 self.assertEquals(stats.record_times, renderer_ref_stats.record_times)
453 self.assertEquals(stats.recorded_pixel_counts,
454 renderer_ref_stats.recorded_pixel_counts)
456 def testInputLatencyFromTimeline(self):
457 timeline = model.TimelineModel()
459 # Create a browser process and a renderer process.
460 browser = timeline.GetOrCreateProcess(pid = 1)
461 browser_main = browser.GetOrCreateThread(tid = 11)
462 renderer = timeline.GetOrCreateProcess(pid = 2)
463 renderer_main = renderer.GetOrCreateThread(tid = 21)
466 ref_latency = ReferenceInputLatencyStats()
468 # Create 10 input latency stats events for Action A.
470 renderer_main.BeginSlice('webkit.console', 'ActionA', timer.Get(), '')
471 for _ in xrange(0, 10):
472 AddInputLatencyStats(timer, browser_main, renderer_main, ref_latency)
474 renderer_main.EndSlice(timer.Get())
476 # Create 5 input latency stats events not within any action.
478 for _ in xrange(0, 5):
479 AddInputLatencyStats(timer, browser_main, renderer_main, None)
481 # Create 10 input latency stats events for Action B.
483 renderer_main.BeginSlice('webkit.console', 'ActionB', timer.Get(), '')
484 for _ in xrange(0, 10):
485 AddInputLatencyStats(timer, browser_main, renderer_main, ref_latency)
487 renderer_main.EndSlice(timer.Get())
489 # Create 10 input latency stats events for Action A.
491 renderer_main.BeginSlice('webkit.console', 'ActionA', timer.Get(), '')
492 for _ in xrange(0, 10):
493 AddInputLatencyStats(timer, browser_main, renderer_main, ref_latency)
495 renderer_main.EndSlice(timer.Get())
497 browser.FinalizeImport()
498 renderer.FinalizeImport()
502 timeline_markers = timeline.FindTimelineMarkers(
503 ['ActionA', 'ActionB', 'ActionA'])
504 timeline_ranges = [timeline_bounds.Bounds.CreateFromEvent(marker)
505 for marker in timeline_markers]
506 for timeline_range in timeline_ranges:
507 if timeline_range.is_empty:
509 input_events.extend(GetInputLatencyEvents(browser, timeline_range))
511 self.assertEquals(input_events, ref_latency.input_event)
512 input_event_latency_result = ComputeInputEventLatencies(input_events)
513 self.assertEquals(input_event_latency_result,
514 ref_latency.input_event_latency)
516 stats = RenderingStats(renderer, browser, timeline_ranges)
517 self.assertEquals(FlattenList(stats.input_event_latency), [
518 latency for name, latency in ref_latency.input_event_latency
519 if name != SCROLL_UPDATE_EVENT_NAME])
520 self.assertEquals(FlattenList(stats.scroll_update_latency), [
521 latency for name, latency in ref_latency.input_event_latency
522 if name == SCROLL_UPDATE_EVENT_NAME])
523 self.assertEquals(FlattenList(stats.gesture_scroll_update_latency), [
524 latency for name, latency in ref_latency.input_event_latency
525 if name == GESTURE_SCROLL_UPDATE_EVENT_NAME])