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 5 import random 6 import unittest 7 8 from telemetry.timeline import async_slice 9 from telemetry.timeline import bounds 10 from telemetry.timeline import model 11 from telemetry.util import perf_tests_helper 12 from telemetry.util import statistics 13 from telemetry.web_perf.metrics import rendering_stats 14 15 16 class MockTimer(object): 17 """A mock timer class which can generate random durations. 18 19 An instance of this class is used as a global timer to generate random 20 durations for stats and consistent timestamps for all mock trace events. 21 The unit of time is milliseconds. 22 """ 23 24 def __init__(self): 25 self.milliseconds = 0 26 27 def Advance(self, low=0.1, high=1): 28 delta = random.uniform(low, high) 29 self.milliseconds += delta 30 return delta 31 32 def AdvanceAndGet(self, low=0.1, high=1): 33 self.Advance(low, high) 34 return self.milliseconds 35 36 37 class ReferenceRenderingStats(object): 38 """ Stores expected data for comparison with actual RenderingStats """ 39 40 def __init__(self): 41 self.frame_timestamps = [] 42 self.frame_times = [] 43 self.approximated_pixel_percentages = [] 44 self.checkerboarded_pixel_percentages = [] 45 46 def AppendNewRange(self): 47 self.frame_timestamps.append([]) 48 self.frame_times.append([]) 49 self.approximated_pixel_percentages.append([]) 50 self.checkerboarded_pixel_percentages.append([]) 51 52 53 class ReferenceInputLatencyStats(object): 54 """ Stores expected data for comparison with actual input latency stats """ 55 56 def __init__(self): 57 self.input_event_latency = [] 58 self.input_event = [] 59 60 61 def AddSurfaceFlingerStats(mock_timer, thread, first_frame, 62 ref_stats=None): 63 """ Adds a random surface flinger stats event. 64 65 thread: The timeline model thread to which the event will be added. 66 first_frame: Is this the first frame within the bounds of an action? 67 ref_stats: A ReferenceRenderingStats object to record expected values. 68 """ 69 # Create randonm data and timestap for impl thread rendering stats. 70 data = {'frame_count': 1, 71 'refresh_period': 16.6666} 72 timestamp = mock_timer.AdvanceAndGet() 73 74 # Add a slice with the event data to the given thread. 75 thread.PushCompleteSlice( 76 'SurfaceFlinger', 'vsync_before', 77 timestamp, duration=0.0, thread_timestamp=None, thread_duration=None, 78 args={'data': data}) 79 80 if not ref_stats: 81 return 82 83 # Add timestamp only if a frame was output 84 if data['frame_count'] == 1: 85 if not first_frame: 86 # Add frame_time if this is not the first frame in within the bounds of an 87 # action. 88 prev_timestamp = ref_stats.frame_timestamps[-1][-1] 89 ref_stats.frame_times[-1].append(timestamp - prev_timestamp) 90 ref_stats.frame_timestamps[-1].append(timestamp) 91 92 93 def AddDisplayRenderingStats(mock_timer, thread, first_frame, 94 ref_stats=None): 95 """ Adds a random display rendering stats event. 96 97 thread: The timeline model thread to which the event will be added. 98 first_frame: Is this the first frame within the bounds of an action? 99 ref_stats: A ReferenceRenderingStats object to record expected values. 100 """ 101 # Create randonm data and timestap for main thread rendering stats. 102 data = {'frame_count': 1} 103 timestamp = mock_timer.AdvanceAndGet() 104 105 # Add a slice with the event data to the given thread. 106 thread.PushCompleteSlice( 107 'benchmark', 'BenchmarkInstrumentation::DisplayRenderingStats', 108 timestamp, duration=0.0, thread_timestamp=None, thread_duration=None, 109 args={'data': data}) 110 111 if not ref_stats: 112 return 113 114 # Add timestamp only if a frame was output 115 if not first_frame: 116 # Add frame_time if this is not the first frame in within the bounds of an 117 # action. 118 prev_timestamp = ref_stats.frame_timestamps[-1][-1] 119 ref_stats.frame_times[-1].append(timestamp - prev_timestamp) 120 ref_stats.frame_timestamps[-1].append(timestamp) 121 122 123 def AddImplThreadRenderingStats(mock_timer, thread, first_frame, 124 ref_stats=None): 125 """ Adds a random impl thread rendering stats event. 126 127 thread: The timeline model thread to which the event will be added. 128 first_frame: Is this the first frame within the bounds of an action? 129 ref_stats: A ReferenceRenderingStats object to record expected values. 130 """ 131 # Create randonm data and timestap for impl thread rendering stats. 132 data = {'frame_count': 1, 133 'visible_content_area': random.uniform(0, 100), 134 'approximated_visible_content_area': random.uniform(0, 5), 135 'checkerboarded_visible_content_area': random.uniform(0, 5)} 136 timestamp = mock_timer.AdvanceAndGet() 137 138 # Add a slice with the event data to the given thread. 139 thread.PushCompleteSlice( 140 'benchmark', 'BenchmarkInstrumentation::ImplThreadRenderingStats', 141 timestamp, duration=0.0, thread_timestamp=None, thread_duration=None, 142 args={'data': data}) 143 144 if not ref_stats: 145 return 146 147 # Add timestamp only if a frame was output 148 if data['frame_count'] == 1: 149 if not first_frame: 150 # Add frame_time if this is not the first frame in within the bounds of an 151 # action. 152 prev_timestamp = ref_stats.frame_timestamps[-1][-1] 153 ref_stats.frame_times[-1].append(timestamp - prev_timestamp) 154 ref_stats.frame_timestamps[-1].append(timestamp) 155 156 ref_stats.approximated_pixel_percentages[-1].append( 157 round(statistics.DivideIfPossibleOrZero( 158 data['approximated_visible_content_area'], 159 data['visible_content_area']) * 100.0, 3)) 160 161 ref_stats.checkerboarded_pixel_percentages[-1].append( 162 round(statistics.DivideIfPossibleOrZero( 163 data['checkerboarded_visible_content_area'], 164 data['visible_content_area']) * 100.0, 3)) 165 166 def AddInputLatencyStats(mock_timer, start_thread, end_thread, 167 ref_latency_stats=None): 168 """ Adds a random input latency stats event. 169 170 start_thread: The start thread on which the async slice is added. 171 end_thread: The end thread on which the async slice is ended. 172 ref_latency_stats: A ReferenceInputLatencyStats object for expected values. 173 """ 174 175 original_comp_time = mock_timer.AdvanceAndGet(2, 4) * 1000.0 176 ui_comp_time = mock_timer.AdvanceAndGet(2, 4) * 1000.0 177 begin_comp_time = mock_timer.AdvanceAndGet(2, 4) * 1000.0 178 forward_comp_time = mock_timer.AdvanceAndGet(2, 4) * 1000.0 179 end_comp_time = mock_timer.AdvanceAndGet(10, 20) * 1000.0 180 181 data = {rendering_stats.ORIGINAL_COMP_NAME: {'time': original_comp_time}, 182 rendering_stats.UI_COMP_NAME: {'time': ui_comp_time}, 183 rendering_stats.BEGIN_COMP_NAME: {'time': begin_comp_time}, 184 rendering_stats.END_COMP_NAME: {'time': end_comp_time}} 185 186 timestamp = mock_timer.AdvanceAndGet(2, 4) 187 188 tracing_async_slice = async_slice.AsyncSlice( 189 'benchmark', 'InputLatency', timestamp) 190 191 async_sub_slice = async_slice.AsyncSlice( 192 'benchmark', rendering_stats.GESTURE_SCROLL_UPDATE_EVENT_NAME, timestamp) 193 async_sub_slice.args = {'data': data} 194 async_sub_slice.parent_slice = tracing_async_slice 195 async_sub_slice.start_thread = start_thread 196 async_sub_slice.end_thread = end_thread 197 198 tracing_async_slice.sub_slices.append(async_sub_slice) 199 tracing_async_slice.start_thread = start_thread 200 tracing_async_slice.end_thread = end_thread 201 start_thread.AddAsyncSlice(tracing_async_slice) 202 203 # Add scroll update latency info. 204 scroll_update_data = { 205 rendering_stats.BEGIN_SCROLL_UPDATE_COMP_NAME: {'time': begin_comp_time}, 206 rendering_stats.FORWARD_SCROLL_UPDATE_COMP_NAME: 207 {'time': forward_comp_time}, 208 rendering_stats.END_COMP_NAME: {'time': end_comp_time} 209 } 210 211 scroll_async_slice = async_slice.AsyncSlice( 212 'benchmark', 'InputLatency', timestamp) 213 214 scroll_async_sub_slice = async_slice.AsyncSlice( 215 'benchmark', rendering_stats.MAIN_THREAD_SCROLL_UPDATE_EVENT_NAME, 216 timestamp) 217 scroll_async_sub_slice.args = {'data': scroll_update_data} 218 scroll_async_sub_slice.parent_slice = scroll_async_slice 219 scroll_async_sub_slice.start_thread = start_thread 220 scroll_async_sub_slice.end_thread = end_thread 221 222 scroll_async_slice.sub_slices.append(scroll_async_sub_slice) 223 scroll_async_slice.start_thread = start_thread 224 scroll_async_slice.end_thread = end_thread 225 start_thread.AddAsyncSlice(scroll_async_slice) 226 227 # Also add some dummy frame statistics so we can feed the resulting timeline 228 # to RenderingStats. 229 AddImplThreadRenderingStats(mock_timer, end_thread, False) 230 231 if not ref_latency_stats: 232 return 233 234 ref_latency_stats.input_event.append(async_sub_slice) 235 ref_latency_stats.input_event.append(scroll_async_sub_slice) 236 ref_latency_stats.input_event_latency.append(( 237 rendering_stats.GESTURE_SCROLL_UPDATE_EVENT_NAME, 238 (data[rendering_stats.END_COMP_NAME]['time'] - 239 data[rendering_stats.ORIGINAL_COMP_NAME]['time']) / 1000.0)) 240 scroll_update_time = ( 241 scroll_update_data[rendering_stats.END_COMP_NAME]['time'] - 242 scroll_update_data[rendering_stats.BEGIN_SCROLL_UPDATE_COMP_NAME]['time']) 243 ref_latency_stats.input_event_latency.append(( 244 rendering_stats.MAIN_THREAD_SCROLL_UPDATE_EVENT_NAME, 245 scroll_update_time / 1000.0)) 246 247 248 class RenderingStatsUnitTest(unittest.TestCase): 249 250 def testHasRenderingStats(self): 251 timeline = model.TimelineModel() 252 timer = MockTimer() 253 254 # A process without rendering stats 255 process_without_stats = timeline.GetOrCreateProcess(pid=1) 256 thread_without_stats = process_without_stats.GetOrCreateThread(tid=11) 257 process_without_stats.FinalizeImport() 258 self.assertFalse(rendering_stats.HasRenderingStats(thread_without_stats)) 259 260 # A process with rendering stats, but no frames in them 261 process_without_frames = timeline.GetOrCreateProcess(pid=2) 262 thread_without_frames = process_without_frames.GetOrCreateThread(tid=21) 263 process_without_frames.FinalizeImport() 264 self.assertFalse(rendering_stats.HasRenderingStats(thread_without_frames)) 265 266 # A process with rendering stats and frames in them 267 process_with_frames = timeline.GetOrCreateProcess(pid=3) 268 thread_with_frames = process_with_frames.GetOrCreateThread(tid=31) 269 AddImplThreadRenderingStats(timer, thread_with_frames, True, None) 270 process_with_frames.FinalizeImport() 271 self.assertTrue(rendering_stats.HasRenderingStats(thread_with_frames)) 272 273 def testBothSurfaceFlingerAndDisplayStats(self): 274 timeline = model.TimelineModel() 275 timer = MockTimer() 276 277 ref_stats = ReferenceRenderingStats() 278 ref_stats.AppendNewRange() 279 surface_flinger = timeline.GetOrCreateProcess(pid=4) 280 surface_flinger.name = 'SurfaceFlinger' 281 surface_flinger_thread = surface_flinger.GetOrCreateThread(tid=41) 282 renderer = timeline.GetOrCreateProcess(pid=2) 283 browser = timeline.GetOrCreateProcess(pid=3) 284 browser_main = browser.GetOrCreateThread(tid=31) 285 browser_main.BeginSlice('webkit.console', 'ActionA', 286 timer.AdvanceAndGet(2, 4), '') 287 288 # Create SurfaceFlinger stats and display rendering stats. 289 for i in xrange(0, 10): 290 first = (i == 0) 291 AddSurfaceFlingerStats(timer, surface_flinger_thread, first, ref_stats) 292 timer.Advance(2, 4) 293 294 for i in xrange(0, 10): 295 first = (i == 0) 296 AddDisplayRenderingStats(timer, browser_main, first, None) 297 timer.Advance(5, 10) 298 299 browser_main.EndSlice(timer.AdvanceAndGet()) 300 timer.Advance(2, 4) 301 302 browser.FinalizeImport() 303 renderer.FinalizeImport() 304 timeline_markers = timeline.FindTimelineMarkers(['ActionA']) 305 timeline_ranges = [bounds.Bounds.CreateFromEvent(marker) 306 for marker in timeline_markers] 307 stats = rendering_stats.RenderingStats( 308 renderer, browser, surface_flinger, timeline_ranges) 309 310 # Compare rendering stats to reference - Only SurfaceFlinger stats should 311 # count 312 self.assertEquals(stats.frame_timestamps, ref_stats.frame_timestamps) 313 self.assertEquals(stats.frame_times, ref_stats.frame_times) 314 315 def testBothDisplayAndImplStats(self): 316 timeline = model.TimelineModel() 317 timer = MockTimer() 318 319 ref_stats = ReferenceRenderingStats() 320 ref_stats.AppendNewRange() 321 renderer = timeline.GetOrCreateProcess(pid=2) 322 browser = timeline.GetOrCreateProcess(pid=3) 323 browser_main = browser.GetOrCreateThread(tid=31) 324 browser_main.BeginSlice('webkit.console', 'ActionA', 325 timer.AdvanceAndGet(2, 4), '') 326 327 # Create main, impl, and display rendering stats. 328 for i in xrange(0, 10): 329 first = (i == 0) 330 AddImplThreadRenderingStats(timer, browser_main, first, None) 331 timer.Advance(2, 4) 332 333 for i in xrange(0, 10): 334 first = (i == 0) 335 AddDisplayRenderingStats(timer, browser_main, first, ref_stats) 336 timer.Advance(5, 10) 337 338 browser_main.EndSlice(timer.AdvanceAndGet()) 339 timer.Advance(2, 4) 340 341 browser.FinalizeImport() 342 renderer.FinalizeImport() 343 timeline_markers = timeline.FindTimelineMarkers(['ActionA']) 344 timeline_ranges = [bounds.Bounds.CreateFromEvent(marker) 345 for marker in timeline_markers] 346 stats = rendering_stats.RenderingStats( 347 renderer, browser, None, timeline_ranges) 348 349 # Compare rendering stats to reference - Only display stats should count 350 self.assertEquals(stats.frame_timestamps, ref_stats.frame_timestamps) 351 self.assertEquals(stats.frame_times, ref_stats.frame_times) 352 353 def testRangeWithoutFrames(self): 354 timer = MockTimer() 355 timeline = model.TimelineModel() 356 357 # Create a renderer process, with a main thread and impl thread. 358 renderer = timeline.GetOrCreateProcess(pid=2) 359 renderer_main = renderer.GetOrCreateThread(tid=21) 360 renderer_compositor = renderer.GetOrCreateThread(tid=22) 361 362 # Create 10 main and impl rendering stats events for Action A. 363 renderer_main.BeginSlice('webkit.console', 'ActionA', 364 timer.AdvanceAndGet(2, 4), '') 365 for i in xrange(0, 10): 366 first = (i == 0) 367 AddImplThreadRenderingStats(timer, renderer_compositor, first, None) 368 renderer_main.EndSlice(timer.AdvanceAndGet(2, 4)) 369 timer.Advance(2, 4) 370 371 # Create 5 main and impl rendering stats events not within any action. 372 for i in xrange(0, 5): 373 first = (i == 0) 374 AddImplThreadRenderingStats(timer, renderer_compositor, first, None) 375 376 # Create Action B without any frames. This should trigger 377 # NotEnoughFramesError when the RenderingStats object is created. 378 renderer_main.BeginSlice('webkit.console', 'ActionB', 379 timer.AdvanceAndGet(2, 4), '') 380 renderer_main.EndSlice(timer.AdvanceAndGet(2, 4)) 381 382 renderer.FinalizeImport() 383 384 timeline_markers = timeline.FindTimelineMarkers(['ActionA', 'ActionB']) 385 timeline_ranges = [bounds.Bounds.CreateFromEvent(marker) 386 for marker in timeline_markers] 387 388 stats = rendering_stats.RenderingStats( 389 renderer, None, None, timeline_ranges) 390 self.assertEquals(0, len(stats.frame_timestamps[1])) 391 392 def testFromTimeline(self): 393 timeline = model.TimelineModel() 394 395 # Create a browser process and a renderer process, and a main thread and 396 # impl thread for each. 397 browser = timeline.GetOrCreateProcess(pid=1) 398 browser_compositor = browser.GetOrCreateThread(tid=12) 399 renderer = timeline.GetOrCreateProcess(pid=2) 400 renderer_main = renderer.GetOrCreateThread(tid=21) 401 renderer_compositor = renderer.GetOrCreateThread(tid=22) 402 403 timer = MockTimer() 404 renderer_ref_stats = ReferenceRenderingStats() 405 browser_ref_stats = ReferenceRenderingStats() 406 407 # Create 10 main and impl rendering stats events for Action A. 408 renderer_main.BeginSlice('webkit.console', 'ActionA', 409 timer.AdvanceAndGet(2, 4), '') 410 renderer_ref_stats.AppendNewRange() 411 browser_ref_stats.AppendNewRange() 412 for i in xrange(0, 10): 413 first = (i == 0) 414 AddImplThreadRenderingStats( 415 timer, renderer_compositor, first, renderer_ref_stats) 416 AddImplThreadRenderingStats( 417 timer, browser_compositor, first, browser_ref_stats) 418 renderer_main.EndSlice(timer.AdvanceAndGet(2, 4)) 419 420 # Create 5 main and impl rendering stats events not within any action. 421 for i in xrange(0, 5): 422 first = (i == 0) 423 AddImplThreadRenderingStats(timer, renderer_compositor, first, None) 424 AddImplThreadRenderingStats(timer, browser_compositor, first, None) 425 426 # Create 10 main and impl rendering stats events for Action B. 427 renderer_main.BeginSlice('webkit.console', 'ActionB', 428 timer.AdvanceAndGet(2, 4), '') 429 renderer_ref_stats.AppendNewRange() 430 browser_ref_stats.AppendNewRange() 431 for i in xrange(0, 10): 432 first = (i == 0) 433 AddImplThreadRenderingStats( 434 timer, renderer_compositor, first, renderer_ref_stats) 435 AddImplThreadRenderingStats( 436 timer, browser_compositor, first, browser_ref_stats) 437 renderer_main.EndSlice(timer.AdvanceAndGet(2, 4)) 438 439 # Create 10 main and impl rendering stats events for Action A. 440 renderer_main.BeginSlice('webkit.console', 'ActionA', 441 timer.AdvanceAndGet(2, 4), '') 442 renderer_ref_stats.AppendNewRange() 443 browser_ref_stats.AppendNewRange() 444 for i in xrange(0, 10): 445 first = (i == 0) 446 AddImplThreadRenderingStats( 447 timer, renderer_compositor, first, renderer_ref_stats) 448 AddImplThreadRenderingStats( 449 timer, browser_compositor, first, browser_ref_stats) 450 renderer_main.EndSlice(timer.AdvanceAndGet(2, 4)) 451 timer.Advance(2, 4) 452 453 browser.FinalizeImport() 454 renderer.FinalizeImport() 455 456 timeline_markers = timeline.FindTimelineMarkers( 457 ['ActionA', 'ActionB', 'ActionA']) 458 timeline_ranges = [bounds.Bounds.CreateFromEvent(marker) 459 for marker in timeline_markers] 460 stats = rendering_stats.RenderingStats( 461 renderer, browser, None, timeline_ranges) 462 463 # Compare rendering stats to reference. 464 self.assertEquals(stats.frame_timestamps, 465 browser_ref_stats.frame_timestamps) 466 self.assertEquals(stats.frame_times, browser_ref_stats.frame_times) 467 self.assertEquals(stats.approximated_pixel_percentages, 468 renderer_ref_stats.approximated_pixel_percentages) 469 self.assertEquals(stats.checkerboarded_pixel_percentages, 470 renderer_ref_stats.checkerboarded_pixel_percentages) 471 472 def testInputLatencyFromTimeline(self): 473 timeline = model.TimelineModel() 474 475 # Create a browser process and a renderer process. 476 browser = timeline.GetOrCreateProcess(pid=1) 477 browser_main = browser.GetOrCreateThread(tid=11) 478 renderer = timeline.GetOrCreateProcess(pid=2) 479 renderer_main = renderer.GetOrCreateThread(tid=21) 480 481 timer = MockTimer() 482 ref_latency = ReferenceInputLatencyStats() 483 484 # Create 10 input latency stats events for Action A. 485 renderer_main.BeginSlice('webkit.console', 'ActionA', 486 timer.AdvanceAndGet(2, 4), '') 487 for _ in xrange(0, 10): 488 AddInputLatencyStats(timer, browser_main, renderer_main, ref_latency) 489 renderer_main.EndSlice(timer.AdvanceAndGet(2, 4)) 490 491 # Create 5 input latency stats events not within any action. 492 timer.Advance(2, 4) 493 for _ in xrange(0, 5): 494 AddInputLatencyStats(timer, browser_main, renderer_main, None) 495 496 # Create 10 input latency stats events for Action B. 497 renderer_main.BeginSlice('webkit.console', 'ActionB', 498 timer.AdvanceAndGet(2, 4), '') 499 for _ in xrange(0, 10): 500 AddInputLatencyStats(timer, browser_main, renderer_main, ref_latency) 501 renderer_main.EndSlice(timer.AdvanceAndGet(2, 4)) 502 503 # Create 10 input latency stats events for Action A. 504 renderer_main.BeginSlice('webkit.console', 'ActionA', 505 timer.AdvanceAndGet(2, 4), '') 506 for _ in xrange(0, 10): 507 AddInputLatencyStats(timer, browser_main, renderer_main, ref_latency) 508 renderer_main.EndSlice(timer.AdvanceAndGet(2, 4)) 509 510 browser.FinalizeImport() 511 renderer.FinalizeImport() 512 513 latency_events = [] 514 515 timeline_markers = timeline.FindTimelineMarkers( 516 ['ActionA', 'ActionB', 'ActionA']) 517 timeline_ranges = [bounds.Bounds.CreateFromEvent(marker) 518 for marker in timeline_markers] 519 for timeline_range in timeline_ranges: 520 if timeline_range.is_empty: 521 continue 522 latency_events.extend(rendering_stats.GetLatencyEvents( 523 browser, timeline_range)) 524 525 self.assertEquals(latency_events, ref_latency.input_event) 526 event_latency_result = rendering_stats.ComputeEventLatencies(latency_events) 527 self.assertEquals(event_latency_result, 528 ref_latency.input_event_latency) 529 530 stats = rendering_stats.RenderingStats( 531 renderer, browser, None, timeline_ranges) 532 self.assertEquals( 533 perf_tests_helper.FlattenList(stats.input_event_latency), 534 [latency for name, latency in ref_latency.input_event_latency 535 if name != rendering_stats.MAIN_THREAD_SCROLL_UPDATE_EVENT_NAME]) 536 self.assertEquals( 537 perf_tests_helper.FlattenList(stats.main_thread_scroll_latency), 538 [latency for name, latency in ref_latency.input_event_latency 539 if name == rendering_stats.MAIN_THREAD_SCROLL_UPDATE_EVENT_NAME]) 540 self.assertEquals( 541 perf_tests_helper.FlattenList(stats.gesture_scroll_update_latency), 542 [latency for name, latency in ref_latency.input_event_latency 543 if name == rendering_stats.GESTURE_SCROLL_UPDATE_EVENT_NAME]) 544