diff options
Diffstat (limited to 'catapult/telemetry/telemetry/web_perf/metrics/rendering_stats.py')
-rw-r--r-- | catapult/telemetry/telemetry/web_perf/metrics/rendering_stats.py | 296 |
1 files changed, 296 insertions, 0 deletions
diff --git a/catapult/telemetry/telemetry/web_perf/metrics/rendering_stats.py b/catapult/telemetry/telemetry/web_perf/metrics/rendering_stats.py new file mode 100644 index 00000000..65bdbee8 --- /dev/null +++ b/catapult/telemetry/telemetry/web_perf/metrics/rendering_stats.py @@ -0,0 +1,296 @@ +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +import itertools + +from operator import attrgetter + +from telemetry.web_perf.metrics import rendering_frame + +# These are LatencyInfo component names indicating the various components +# that the input event has travelled through. +# This is when the input event first reaches chrome. +UI_COMP_NAME = 'INPUT_EVENT_LATENCY_UI_COMPONENT' +# This is when the input event was originally created by OS. +ORIGINAL_COMP_NAME = 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT' +# This is when the input event was sent from browser to renderer. +BEGIN_COMP_NAME = 'INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT' +# This is when an input event is turned into a scroll update. +BEGIN_SCROLL_UPDATE_COMP_NAME = ( + 'LATENCY_BEGIN_SCROLL_LISTENER_UPDATE_MAIN_COMPONENT') +# This is when a scroll update is forwarded to the main thread. +FORWARD_SCROLL_UPDATE_COMP_NAME = ( + 'INPUT_EVENT_LATENCY_FORWARD_SCROLL_UPDATE_TO_MAIN_COMPONENT') +# This is when the input event has reached swap buffer. +END_COMP_NAME = 'INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT' + +# Name for a main thread scroll update latency event. +MAIN_THREAD_SCROLL_UPDATE_EVENT_NAME = 'Latency::ScrollUpdate' +# Name for a gesture scroll update latency event. +GESTURE_SCROLL_UPDATE_EVENT_NAME = 'InputLatency::GestureScrollUpdate' + +# These are keys used in the 'data' field dictionary located in +# BenchmarkInstrumentation::ImplThreadRenderingStats. +VISIBLE_CONTENT_DATA = 'visible_content_area' +APPROXIMATED_VISIBLE_CONTENT_DATA = 'approximated_visible_content_area' +CHECKERBOARDED_VISIBLE_CONTENT_DATA = 'checkerboarded_visible_content_area' +# These are keys used in the 'errors' field dictionary located in +# RenderingStats in this file. +APPROXIMATED_PIXEL_ERROR = 'approximated_pixel_percentages' +CHECKERBOARDED_PIXEL_ERROR = 'checkerboarded_pixel_percentages' + + +def GetLatencyEvents(process, timeline_range): + """Get LatencyInfo trace events from the process's trace buffer that are + within the timeline_range. + + Input events dump their LatencyInfo into trace buffer as async trace event + of name starting with "InputLatency". Non-input events with name starting + with "Latency". The trace event has a member 'data' containing its latency + history. + + """ + latency_events = [] + if not process: + return latency_events + for event in itertools.chain( + process.IterAllAsyncSlicesStartsWithName('InputLatency'), + process.IterAllAsyncSlicesStartsWithName('Latency')): + if event.start >= timeline_range.min and event.end <= timeline_range.max: + for ss in event.sub_slices: + if 'data' in ss.args: + latency_events.append(ss) + return latency_events + + +def ComputeEventLatencies(input_events): + """ Compute input event latencies. + + Input event latency is the time from when the input event is created to + when its resulted page is swap buffered. + Input event on different platforms uses different LatencyInfo component to + record its creation timestamp. We go through the following component list + to find the creation timestamp: + 1. INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT -- when event is created in OS + 2. INPUT_EVENT_LATENCY_UI_COMPONENT -- when event reaches Chrome + 3. INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT -- when event reaches RenderWidget + + If the latency starts with a + LATENCY_BEGIN_SCROLL_UPDATE_MAIN_COMPONENT component, then it is + classified as a scroll update instead of a normal input latency measure. + + Returns: + A list sorted by increasing start time of latencies which are tuples of + (input_event_name, latency_in_ms). + """ + input_event_latencies = [] + for event in input_events: + data = event.args['data'] + if END_COMP_NAME in data: + end_time = data[END_COMP_NAME]['time'] + if ORIGINAL_COMP_NAME in data: + start_time = data[ORIGINAL_COMP_NAME]['time'] + elif UI_COMP_NAME in data: + start_time = data[UI_COMP_NAME]['time'] + elif BEGIN_COMP_NAME in data: + start_time = data[BEGIN_COMP_NAME]['time'] + elif BEGIN_SCROLL_UPDATE_COMP_NAME in data: + start_time = data[BEGIN_SCROLL_UPDATE_COMP_NAME]['time'] + else: + raise ValueError('LatencyInfo has no begin component') + latency = (end_time - start_time) / 1000.0 + input_event_latencies.append((start_time, event.name, latency)) + + input_event_latencies.sort() + return [(name, latency) for _, name, latency in input_event_latencies] + + +def HasRenderingStats(process): + """ Returns True if the process contains at least one + BenchmarkInstrumentation::*RenderingStats event with a frame. + """ + if not process: + return False + for event in process.IterAllSlicesOfName( + 'BenchmarkInstrumentation::DisplayRenderingStats'): + if 'data' in event.args and event.args['data']['frame_count'] == 1: + return True + for event in process.IterAllSlicesOfName( + 'BenchmarkInstrumentation::ImplThreadRenderingStats'): + if 'data' in event.args and event.args['data']['frame_count'] == 1: + return True + return False + +def GetTimestampEventName(process): + """ Returns the name of the events used to count frame timestamps. """ + if process.name == 'SurfaceFlinger': + return 'vsync_before' + + event_name = 'BenchmarkInstrumentation::DisplayRenderingStats' + for event in process.IterAllSlicesOfName(event_name): + if 'data' in event.args and event.args['data']['frame_count'] == 1: + return event_name + + return 'BenchmarkInstrumentation::ImplThreadRenderingStats' + +class RenderingStats(object): + def __init__(self, renderer_process, browser_process, surface_flinger_process, + timeline_ranges): + """ + Utility class for extracting rendering statistics from the timeline (or + other loggin facilities), and providing them in a common format to classes + that compute benchmark metrics from this data. + + Stats are lists of lists of numbers. The outer list stores one list per + timeline range. + + All *_time values are measured in milliseconds. + """ + assert len(timeline_ranges) > 0 + self.refresh_period = None + + # Find the top level process with rendering stats (browser or renderer). + if surface_flinger_process: + timestamp_process = surface_flinger_process + self._GetRefreshPeriodFromSurfaceFlingerProcess(surface_flinger_process) + elif HasRenderingStats(browser_process): + timestamp_process = browser_process + else: + timestamp_process = renderer_process + + timestamp_event_name = GetTimestampEventName(timestamp_process) + + # A lookup from list names below to any errors or exceptions encountered + # in attempting to generate that list. + self.errors = {} + + self.frame_timestamps = [] + self.frame_times = [] + self.approximated_pixel_percentages = [] + self.checkerboarded_pixel_percentages = [] + # End-to-end latency for input event - from when input event is + # generated to when the its resulted page is swap buffered. + self.input_event_latency = [] + self.frame_queueing_durations = [] + # Latency from when a scroll update is sent to the main thread until the + # resulting frame is swapped. + self.main_thread_scroll_latency = [] + # Latency for a GestureScrollUpdate input event. + self.gesture_scroll_update_latency = [] + + for timeline_range in timeline_ranges: + self.frame_timestamps.append([]) + self.frame_times.append([]) + self.approximated_pixel_percentages.append([]) + self.checkerboarded_pixel_percentages.append([]) + self.input_event_latency.append([]) + self.main_thread_scroll_latency.append([]) + self.gesture_scroll_update_latency.append([]) + + if timeline_range.is_empty: + continue + self._InitFrameTimestampsFromTimeline( + timestamp_process, timestamp_event_name, timeline_range) + self._InitImplThreadRenderingStatsFromTimeline( + renderer_process, timeline_range) + self._InitInputLatencyStatsFromTimeline( + browser_process, renderer_process, timeline_range) + self._InitFrameQueueingDurationsFromTimeline( + renderer_process, timeline_range) + + def _GetRefreshPeriodFromSurfaceFlingerProcess(self, surface_flinger_process): + for event in surface_flinger_process.IterAllEventsOfName('vsync_before'): + self.refresh_period = event.args['data']['refresh_period'] + return + + def _InitInputLatencyStatsFromTimeline( + self, browser_process, renderer_process, timeline_range): + latency_events = GetLatencyEvents(browser_process, timeline_range) + # Plugin input event's latency slice is generated in renderer process. + latency_events.extend(GetLatencyEvents(renderer_process, timeline_range)) + event_latencies = ComputeEventLatencies(latency_events) + # Don't include scroll updates in the overall input latency measurement, + # because scroll updates can take much more time to process than other + # input events and would therefore add noise to overall latency numbers. + self.input_event_latency[-1] = [ + latency for name, latency in event_latencies + if name != MAIN_THREAD_SCROLL_UPDATE_EVENT_NAME] + self.main_thread_scroll_latency[-1] = [ + latency for name, latency in event_latencies + if name == MAIN_THREAD_SCROLL_UPDATE_EVENT_NAME] + self.gesture_scroll_update_latency[-1] = [ + latency for name, latency in event_latencies + if name == GESTURE_SCROLL_UPDATE_EVENT_NAME] + + def _GatherEvents(self, event_name, process, timeline_range): + events = [] + for event in process.IterAllSlicesOfName(event_name): + if event.start >= timeline_range.min and event.end <= timeline_range.max: + if 'data' not in event.args: + continue + events.append(event) + events.sort(key=attrgetter('start')) + return events + + def _AddFrameTimestamp(self, event): + frame_count = event.args['data']['frame_count'] + if frame_count > 1: + raise ValueError('trace contains multi-frame render stats') + if frame_count == 1: + self.frame_timestamps[-1].append( + event.start) + if len(self.frame_timestamps[-1]) >= 2: + self.frame_times[-1].append( + self.frame_timestamps[-1][-1] - self.frame_timestamps[-1][-2]) + + def _InitFrameTimestampsFromTimeline( + self, process, timestamp_event_name, timeline_range): + for event in self._GatherEvents( + timestamp_event_name, process, timeline_range): + self._AddFrameTimestamp(event) + + def _InitImplThreadRenderingStatsFromTimeline(self, process, timeline_range): + event_name = 'BenchmarkInstrumentation::ImplThreadRenderingStats' + for event in self._GatherEvents(event_name, process, timeline_range): + data = event.args['data'] + if VISIBLE_CONTENT_DATA not in data: + self.errors[APPROXIMATED_PIXEL_ERROR] = ( + 'Calculating approximated_pixel_percentages not possible because ' + 'visible_content_area was missing.') + self.errors[CHECKERBOARDED_PIXEL_ERROR] = ( + 'Calculating checkerboarded_pixel_percentages not possible because ' + 'visible_content_area was missing.') + return + visible_content_area = data[VISIBLE_CONTENT_DATA] + if visible_content_area == 0: + self.errors[APPROXIMATED_PIXEL_ERROR] = ( + 'Calculating approximated_pixel_percentages would have caused ' + 'a divide-by-zero') + self.errors[CHECKERBOARDED_PIXEL_ERROR] = ( + 'Calculating checkerboarded_pixel_percentages would have caused ' + 'a divide-by-zero') + return + if APPROXIMATED_VISIBLE_CONTENT_DATA in data: + self.approximated_pixel_percentages[-1].append( + round(float(data[APPROXIMATED_VISIBLE_CONTENT_DATA]) / + float(data[VISIBLE_CONTENT_DATA]) * 100.0, 3)) + else: + self.errors[APPROXIMATED_PIXEL_ERROR] = ( + 'approximated_pixel_percentages was not recorded') + if CHECKERBOARDED_VISIBLE_CONTENT_DATA in data: + self.checkerboarded_pixel_percentages[-1].append( + round(float(data[CHECKERBOARDED_VISIBLE_CONTENT_DATA]) / + float(data[VISIBLE_CONTENT_DATA]) * 100.0, 3)) + else: + self.errors[CHECKERBOARDED_PIXEL_ERROR] = ( + 'checkerboarded_pixel_percentages was not recorded') + + def _InitFrameQueueingDurationsFromTimeline(self, process, timeline_range): + try: + events = rendering_frame.GetFrameEventsInsideRange(process, + timeline_range) + new_frame_queueing_durations = [e.queueing_duration for e in events] + self.frame_queueing_durations.append(new_frame_queueing_durations) + except rendering_frame.NoBeginFrameIdException: + self.errors['frame_queueing_durations'] = ( + 'Current chrome version does not support the queueing delay metric.') |