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. 4from operator import attrgetter 5 6from telemetry.web_perf.metrics import rendering_frame 7 8# These are LatencyInfo component names indicating the various components 9# that the input event has travelled through. 10# This is when the input event first reaches chrome. 11UI_COMP_NAME = 'INPUT_EVENT_LATENCY_UI_COMPONENT' 12# This is when the input event was originally created by OS. 13ORIGINAL_COMP_NAME = 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT' 14# This is when the input event was sent from browser to renderer. 15BEGIN_COMP_NAME = 'INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT' 16# This is when an input event is turned into a scroll update. 17BEGIN_SCROLL_UPDATE_COMP_NAME = ( 18 'INPUT_EVENT_LATENCY_BEGIN_SCROLL_UPDATE_MAIN_COMPONENT') 19# This is when a scroll update is forwarded to the main thread. 20FORWARD_SCROLL_UPDATE_COMP_NAME = ( 21 'INPUT_EVENT_LATENCY_FORWARD_SCROLL_UPDATE_TO_MAIN_COMPONENT') 22# This is when the input event has reached swap buffer. 23END_COMP_NAME = 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT' 24 25# Name for a main thread scroll update latency event. 26SCROLL_UPDATE_EVENT_NAME = 'InputLatency:ScrollUpdate' 27# Name for a gesture scroll update latency event. 28GESTURE_SCROLL_UPDATE_EVENT_NAME = 'InputLatency:GestureScrollUpdate' 29 30 31def GetInputLatencyEvents(process, timeline_range): 32 """Get input events' LatencyInfo from the process's trace buffer that are 33 within the timeline_range. 34 35 Input events dump their LatencyInfo into trace buffer as async trace event 36 with name "InputLatency". The trace event has a memeber 'data' containing 37 its latency history. 38 39 """ 40 input_events = [] 41 if not process: 42 return input_events 43 for event in process.IterAllAsyncSlicesOfName('InputLatency'): 44 if event.start >= timeline_range.min and event.end <= timeline_range.max: 45 for ss in event.sub_slices: 46 if 'data' in ss.args: 47 input_events.append(ss) 48 return input_events 49 50 51def ComputeInputEventLatencies(input_events): 52 """ Compute input event latencies. 53 54 Input event latency is the time from when the input event is created to 55 when its resulted page is swap buffered. 56 Input event on differnt platforms uses different LatencyInfo component to 57 record its creation timestamp. We go through the following component list 58 to find the creation timestamp: 59 1. INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT -- when event is created in OS 60 2. INPUT_EVENT_LATENCY_UI_COMPONENT -- when event reaches Chrome 61 3. INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT -- when event reaches RenderWidget 62 63 If the latency starts with a 64 INPUT_EVENT_LATENCY_BEGIN_SCROLL_UPDATE_MAIN_COMPONENT component, then it is 65 classified as a scroll update instead of a normal input latency measure. 66 67 Returns: 68 A list sorted by increasing start time of latencies which are tuples of 69 (input_event_name, latency_in_ms). 70 """ 71 input_event_latencies = [] 72 for event in input_events: 73 data = event.args['data'] 74 if END_COMP_NAME in data: 75 end_time = data[END_COMP_NAME]['time'] 76 if ORIGINAL_COMP_NAME in data: 77 start_time = data[ORIGINAL_COMP_NAME]['time'] 78 elif UI_COMP_NAME in data: 79 start_time = data[UI_COMP_NAME]['time'] 80 elif BEGIN_COMP_NAME in data: 81 start_time = data[BEGIN_COMP_NAME]['time'] 82 elif BEGIN_SCROLL_UPDATE_COMP_NAME in data: 83 start_time = data[BEGIN_SCROLL_UPDATE_COMP_NAME]['time'] 84 else: 85 raise ValueError, 'LatencyInfo has no begin component' 86 latency = (end_time - start_time) / 1000.0 87 input_event_latencies.append((start_time, event.name, latency)) 88 89 input_event_latencies.sort() 90 return [(name, latency) for _, name, latency in input_event_latencies] 91 92 93def HasRenderingStats(process): 94 """ Returns True if the process contains at least one 95 BenchmarkInstrumentation::*RenderingStats event with a frame. 96 """ 97 if not process: 98 return False 99 for event in process.IterAllSlicesOfName( 100 'BenchmarkInstrumentation::MainThreadRenderingStats'): 101 if 'data' in event.args and event.args['data']['frame_count'] == 1: 102 return True 103 for event in process.IterAllSlicesOfName( 104 'BenchmarkInstrumentation::ImplThreadRenderingStats'): 105 if 'data' in event.args and event.args['data']['frame_count'] == 1: 106 return True 107 return False 108 109 110class RenderingStats(object): 111 def __init__(self, renderer_process, browser_process, timeline_ranges): 112 """ 113 Utility class for extracting rendering statistics from the timeline (or 114 other loggin facilities), and providing them in a common format to classes 115 that compute benchmark metrics from this data. 116 117 Stats are lists of lists of numbers. The outer list stores one list per 118 timeline range. 119 120 All *_time values are measured in milliseconds. 121 """ 122 assert(len(timeline_ranges) > 0) 123 # Find the top level process with rendering stats (browser or renderer). 124 if HasRenderingStats(browser_process): 125 timestamp_process = browser_process 126 else: 127 timestamp_process = renderer_process 128 129 # A lookup from list names below to any errors or exceptions encountered 130 # in attempting to generate that list. 131 self.errors = {} 132 133 self.frame_timestamps = [] 134 self.frame_times = [] 135 self.paint_times = [] 136 self.painted_pixel_counts = [] 137 self.record_times = [] 138 self.recorded_pixel_counts = [] 139 self.rasterize_times = [] 140 self.rasterized_pixel_counts = [] 141 self.approximated_pixel_percentages = [] 142 # End-to-end latency for input event - from when input event is 143 # generated to when the its resulted page is swap buffered. 144 self.input_event_latency = [] 145 self.frame_queueing_durations = [] 146 # Latency from when a scroll update is sent to the main thread until the 147 # resulting frame is swapped. 148 self.scroll_update_latency = [] 149 # Latency for a GestureScrollUpdate input event. 150 self.gesture_scroll_update_latency = [] 151 152 for timeline_range in timeline_ranges: 153 self.frame_timestamps.append([]) 154 self.frame_times.append([]) 155 self.paint_times.append([]) 156 self.painted_pixel_counts.append([]) 157 self.record_times.append([]) 158 self.recorded_pixel_counts.append([]) 159 self.rasterize_times.append([]) 160 self.rasterized_pixel_counts.append([]) 161 self.approximated_pixel_percentages.append([]) 162 self.input_event_latency.append([]) 163 self.scroll_update_latency.append([]) 164 self.gesture_scroll_update_latency.append([]) 165 166 if timeline_range.is_empty: 167 continue 168 self._InitFrameTimestampsFromTimeline(timestamp_process, timeline_range) 169 self._InitMainThreadRenderingStatsFromTimeline( 170 renderer_process, timeline_range) 171 self._InitImplThreadRenderingStatsFromTimeline( 172 renderer_process, timeline_range) 173 self._InitInputLatencyStatsFromTimeline( 174 browser_process, renderer_process, timeline_range) 175 self._InitFrameQueueingDurationsFromTimeline( 176 renderer_process, timeline_range) 177 178 def _InitInputLatencyStatsFromTimeline( 179 self, browser_process, renderer_process, timeline_range): 180 latency_events = GetInputLatencyEvents(browser_process, timeline_range) 181 # Plugin input event's latency slice is generated in renderer process. 182 latency_events.extend(GetInputLatencyEvents(renderer_process, 183 timeline_range)) 184 input_event_latencies = ComputeInputEventLatencies(latency_events) 185 # Don't include scroll updates in the overall input latency measurement, 186 # because scroll updates can take much more time to process than other 187 # input events and would therefore add noise to overall latency numbers. 188 self.input_event_latency[-1] = [ 189 latency for name, latency in input_event_latencies 190 if name != SCROLL_UPDATE_EVENT_NAME] 191 self.scroll_update_latency[-1] = [ 192 latency for name, latency in input_event_latencies 193 if name == SCROLL_UPDATE_EVENT_NAME] 194 self.gesture_scroll_update_latency[-1] = [ 195 latency for name, latency in input_event_latencies 196 if name == GESTURE_SCROLL_UPDATE_EVENT_NAME] 197 198 def _GatherEvents(self, event_name, process, timeline_range): 199 events = [] 200 for event in process.IterAllSlicesOfName(event_name): 201 if event.start >= timeline_range.min and event.end <= timeline_range.max: 202 if 'data' not in event.args: 203 continue 204 events.append(event) 205 events.sort(key=attrgetter('start')) 206 return events 207 208 def _AddFrameTimestamp(self, event): 209 frame_count = event.args['data']['frame_count'] 210 if frame_count > 1: 211 raise ValueError('trace contains multi-frame render stats') 212 if frame_count == 1: 213 self.frame_timestamps[-1].append( 214 event.start) 215 if len(self.frame_timestamps[-1]) >= 2: 216 self.frame_times[-1].append(round(self.frame_timestamps[-1][-1] - 217 self.frame_timestamps[-1][-2], 2)) 218 219 def _InitFrameTimestampsFromTimeline(self, process, timeline_range): 220 event_name = 'BenchmarkInstrumentation::MainThreadRenderingStats' 221 for event in self._GatherEvents(event_name, process, timeline_range): 222 self._AddFrameTimestamp(event) 223 224 event_name = 'BenchmarkInstrumentation::ImplThreadRenderingStats' 225 for event in self._GatherEvents(event_name, process, timeline_range): 226 self._AddFrameTimestamp(event) 227 228 def _InitMainThreadRenderingStatsFromTimeline(self, process, timeline_range): 229 event_name = 'BenchmarkInstrumentation::MainThreadRenderingStats' 230 for event in self._GatherEvents(event_name, process, timeline_range): 231 data = event.args['data'] 232 self.paint_times[-1].append(1000.0 * data['paint_time']) 233 self.painted_pixel_counts[-1].append(data['painted_pixel_count']) 234 self.record_times[-1].append(1000.0 * data['record_time']) 235 self.recorded_pixel_counts[-1].append(data['recorded_pixel_count']) 236 237 def _InitImplThreadRenderingStatsFromTimeline(self, process, timeline_range): 238 event_name = 'BenchmarkInstrumentation::ImplThreadRenderingStats' 239 for event in self._GatherEvents(event_name, process, timeline_range): 240 data = event.args['data'] 241 self.rasterize_times[-1].append(1000.0 * data['rasterize_time']) 242 self.rasterized_pixel_counts[-1].append(data['rasterized_pixel_count']) 243 if data.get('visible_content_area', 0): 244 self.approximated_pixel_percentages[-1].append( 245 round(float(data['approximated_visible_content_area']) / 246 float(data['visible_content_area']) * 100.0, 3)) 247 else: 248 self.approximated_pixel_percentages[-1].append(0.0) 249 250 def _InitFrameQueueingDurationsFromTimeline(self, process, timeline_range): 251 try: 252 events = rendering_frame.GetFrameEventsInsideRange(process, 253 timeline_range) 254 new_frame_queueing_durations = [e.queueing_duration for e in events] 255 self.frame_queueing_durations.append(new_frame_queueing_durations) 256 except rendering_frame.NoBeginFrameIdException: 257 self.errors['frame_queueing_durations'] = ( 258 'Current chrome version does not support the queueing delay metric.') 259