|  | # Copyright 2016 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. | 
|  |  | 
|  | """Pull a sandwich run's output directory's metrics from traces into a CSV. | 
|  |  | 
|  | python pull_sandwich_metrics.py -h | 
|  | """ | 
|  |  | 
|  | import collections | 
|  | import json | 
|  | import logging | 
|  | import os | 
|  | import shutil | 
|  | import subprocess | 
|  | import sys | 
|  | import tempfile | 
|  |  | 
|  | _SRC_DIR = os.path.abspath(os.path.join( | 
|  | os.path.dirname(__file__), '..', '..', '..')) | 
|  |  | 
|  | sys.path.append(os.path.join(_SRC_DIR, 'tools', 'perf')) | 
|  | from chrome_telemetry_build import chromium_config | 
|  |  | 
|  | sys.path.append(chromium_config.GetTelemetryDir()) | 
|  | from telemetry.internal.image_processing import video | 
|  | from telemetry.util import image_util | 
|  | from telemetry.util import rgba_color | 
|  |  | 
|  | import common_util | 
|  | import loading_trace as loading_trace_module | 
|  | import sandwich_runner | 
|  | import tracing | 
|  |  | 
|  |  | 
|  | COMMON_CSV_COLUMN_NAMES = [ | 
|  | 'chromium_commit', | 
|  | 'platform', | 
|  | 'first_layout', | 
|  | 'first_contentful_paint', | 
|  | 'first_meaningful_paint', | 
|  | 'total_load', | 
|  | 'js_onload_event', | 
|  | 'browser_malloc_avg', | 
|  | 'browser_malloc_max', | 
|  | 'speed_index', | 
|  | 'net_emul.name', # Should be in emulation.NETWORK_CONDITIONS.keys() | 
|  | 'net_emul.download', | 
|  | 'net_emul.upload', | 
|  | 'net_emul.latency'] | 
|  |  | 
|  | _UNAVAILABLE_CSV_VALUE = 'unavailable' | 
|  |  | 
|  | _FAILED_CSV_VALUE = 'failed' | 
|  |  | 
|  | _TRACKED_EVENT_NAMES = set([ | 
|  | 'requestStart', | 
|  | 'loadEventStart', | 
|  | 'loadEventEnd', | 
|  | 'firstContentfulPaint', | 
|  | 'firstLayout']) | 
|  |  | 
|  | # Points of a completeness record. | 
|  | # | 
|  | # Members: | 
|  | #   |time| is in milliseconds, | 
|  | #   |frame_completeness| value representing how complete the frame is at a given | 
|  | #     |time|. Caution: this completeness might be negative. | 
|  | CompletenessPoint = collections.namedtuple('CompletenessPoint', | 
|  | ('time', 'frame_completeness')) | 
|  |  | 
|  |  | 
|  | def _GetBrowserPID(tracing_track): | 
|  | """Get the browser PID from a trace. | 
|  |  | 
|  | Args: | 
|  | tracing_track: The tracing.TracingTrack. | 
|  |  | 
|  | Returns: | 
|  | The browser's PID as an integer. | 
|  | """ | 
|  | for event in tracing_track.GetEvents(): | 
|  | if event.category != '__metadata' or event.name != 'process_name': | 
|  | continue | 
|  | if event.args['name'] == 'Browser': | 
|  | return event.pid | 
|  | raise ValueError('couldn\'t find browser\'s PID') | 
|  |  | 
|  |  | 
|  | def _GetBrowserDumpEvents(tracing_track): | 
|  | """Get the browser memory dump events from a tracing track. | 
|  |  | 
|  | Args: | 
|  | tracing_track: The tracing.TracingTrack. | 
|  |  | 
|  | Returns: | 
|  | List of memory dump events. | 
|  | """ | 
|  | assert sandwich_runner.MEMORY_DUMP_CATEGORY in tracing_track.Categories() | 
|  | browser_pid = _GetBrowserPID(tracing_track) | 
|  | browser_dumps_events = [] | 
|  | for event in tracing_track.GetEvents(): | 
|  | if event.category != 'disabled-by-default-memory-infra': | 
|  | continue | 
|  | if event.type != 'v' or event.name != 'periodic_interval': | 
|  | continue | 
|  | # Ignore dump events for processes other than the browser process | 
|  | if event.pid != browser_pid: | 
|  | continue | 
|  | browser_dumps_events.append(event) | 
|  | if len(browser_dumps_events) == 0: | 
|  | raise ValueError('No browser dump events found.') | 
|  | return browser_dumps_events | 
|  |  | 
|  |  | 
|  | def _GetWebPageTrackedEvents(tracing_track): | 
|  | """Get the web page's tracked events from a tracing track. | 
|  |  | 
|  | Args: | 
|  | tracing_track: The tracing.TracingTrack. | 
|  |  | 
|  | Returns: | 
|  | A dict mapping event.name -> tracing.Event for each first occurrence of a | 
|  | tracked event. | 
|  | """ | 
|  | main_frame_id = None | 
|  | tracked_events = {} | 
|  | sorted_events = sorted(tracing_track.GetEvents(), | 
|  | key=lambda event: event.start_msec) | 
|  | for event in sorted_events: | 
|  | if event.category != 'blink.user_timing': | 
|  | continue | 
|  | event_name = event.name | 
|  |  | 
|  | # Find the id of the main frame. Skip all events until it is found. | 
|  | if not main_frame_id: | 
|  | # Tracing (in Sandwich) is started after about:blank is fully loaded, | 
|  | # hence the first navigationStart in the trace registers the correct frame | 
|  | # id. | 
|  | if event_name == 'navigationStart': | 
|  | logging.info('  Found navigationStart at: %f', event.start_msec) | 
|  | main_frame_id = event.args['frame'] | 
|  | continue | 
|  |  | 
|  | # Ignore events with frame id attached, but not being the main frame. | 
|  | if 'frame' in event.args and event.args['frame'] != main_frame_id: | 
|  | continue | 
|  |  | 
|  | # Capture trace events by the first time of their appearance. Note: some | 
|  | # important events (like requestStart) do not have a frame id attached. | 
|  | if event_name in _TRACKED_EVENT_NAMES and event_name not in tracked_events: | 
|  | tracked_events[event_name] = event | 
|  | logging.info('  Event %s first appears at: %f', event_name, | 
|  | event.start_msec) | 
|  | return tracked_events | 
|  |  | 
|  |  | 
|  | def _ExtractDefaultMetrics(loading_trace): | 
|  | """Extracts all the default metrics from a given trace. | 
|  |  | 
|  | Args: | 
|  | loading_trace: loading_trace.LoadingTrace. | 
|  |  | 
|  | Returns: | 
|  | Dictionary with all trace extracted fields set. | 
|  | """ | 
|  | END_REQUEST_EVENTS = [ | 
|  | ('first_layout', 'requestStart', 'firstLayout'), | 
|  | ('first_contentful_paint', 'requestStart', 'firstContentfulPaint'), | 
|  | ('total_load', 'requestStart', 'loadEventEnd'), | 
|  | ('js_onload_event', 'loadEventStart', 'loadEventEnd')] | 
|  | web_page_tracked_events = _GetWebPageTrackedEvents( | 
|  | loading_trace.tracing_track) | 
|  | metrics = {} | 
|  | for metric_name, start_event_name, end_event_name in END_REQUEST_EVENTS: | 
|  | try: | 
|  | metrics[metric_name] = ( | 
|  | web_page_tracked_events[end_event_name].start_msec - | 
|  | web_page_tracked_events[start_event_name].start_msec) | 
|  | except KeyError as error: | 
|  | logging.error('could not extract metric %s: missing trace event: %s' % ( | 
|  | metric_name, str(error))) | 
|  | metrics[metric_name] = _FAILED_CSV_VALUE | 
|  | return metrics | 
|  |  | 
|  |  | 
|  | def _ExtractTimeToFirstMeaningfulPaint(loading_trace): | 
|  | """Extracts the time to first meaningful paint from a given trace. | 
|  |  | 
|  | Args: | 
|  | loading_trace: loading_trace_module.LoadingTrace. | 
|  |  | 
|  | Returns: | 
|  | Time to first meaningful paint in milliseconds. | 
|  | """ | 
|  | required_categories = set(sandwich_runner.TTFMP_ADDITIONAL_CATEGORIES) | 
|  | if not required_categories.issubset(loading_trace.tracing_track.Categories()): | 
|  | return _UNAVAILABLE_CSV_VALUE | 
|  | logging.info('  Extracting first_meaningful_paint') | 
|  | events = [e.ToJsonDict() for e in loading_trace.tracing_track.GetEvents()] | 
|  | with common_util.TemporaryDirectory(prefix='sandwich_tmp_') as tmp_dir: | 
|  | chrome_trace_path = os.path.join(tmp_dir, 'chrome_trace.json') | 
|  | with open(chrome_trace_path, 'w') as output_file: | 
|  | json.dump({'traceEvents': events, 'metadata': {}}, output_file) | 
|  | catapult_run_metric_bin_path = os.path.join( | 
|  | _SRC_DIR, 'third_party', 'catapult', 'tracing', 'bin', 'run_metric') | 
|  | output = subprocess.check_output( | 
|  | [catapult_run_metric_bin_path, 'firstPaintMetric', chrome_trace_path]) | 
|  | json_output = json.loads(output) | 
|  | for metric in json_output[chrome_trace_path]['pairs']['values']: | 
|  | if metric['name'] == 'firstMeaningfulPaint_avg': | 
|  | return metric['numeric']['value'] | 
|  | logging.info('  Extracting first_meaningful_paint: failed') | 
|  | return _FAILED_CSV_VALUE | 
|  |  | 
|  |  | 
|  | def _ExtractMemoryMetrics(loading_trace): | 
|  | """Extracts all the memory metrics from a given trace. | 
|  |  | 
|  | Args: | 
|  | loading_trace: loading_trace_module.LoadingTrace. | 
|  |  | 
|  | Returns: | 
|  | Dictionary with all trace extracted fields set. | 
|  | """ | 
|  | if (sandwich_runner.MEMORY_DUMP_CATEGORY not in | 
|  | loading_trace.tracing_track.Categories()): | 
|  | return { | 
|  | 'browser_malloc_avg': _UNAVAILABLE_CSV_VALUE, | 
|  | 'browser_malloc_max': _UNAVAILABLE_CSV_VALUE | 
|  | } | 
|  | browser_dump_events = _GetBrowserDumpEvents(loading_trace.tracing_track) | 
|  | browser_malloc_sum = 0 | 
|  | browser_malloc_max = 0 | 
|  | for dump_event in browser_dump_events: | 
|  | attr = dump_event.args['dumps']['allocators']['malloc']['attrs']['size'] | 
|  | assert attr['units'] == 'bytes' | 
|  | size = int(attr['value'], 16) | 
|  | browser_malloc_sum += size | 
|  | browser_malloc_max = max(browser_malloc_max, size) | 
|  | return { | 
|  | 'browser_malloc_avg': browser_malloc_sum / float(len(browser_dump_events)), | 
|  | 'browser_malloc_max': browser_malloc_max | 
|  | } | 
|  |  | 
|  |  | 
|  | def _ExtractCompletenessRecordFromVideo(video_path): | 
|  | """Extracts the completeness record from a video. | 
|  |  | 
|  | The video must start with a filled rectangle of orange (RGB: 222, 100, 13), to | 
|  | give the view-port size/location from where to compute the completeness. | 
|  |  | 
|  | Args: | 
|  | video_path: Path of the video to extract the completeness list from. | 
|  |  | 
|  | Returns: | 
|  | list(CompletenessPoint) | 
|  | """ | 
|  | video_file = tempfile.NamedTemporaryFile() | 
|  | shutil.copy(video_path, video_file.name) | 
|  | video_capture = video.Video(video_file) | 
|  |  | 
|  | histograms = [ | 
|  | (time, image_util.GetColorHistogram( | 
|  | image, ignore_color=rgba_color.WHITE, tolerance=8)) | 
|  | for time, image in video_capture.GetVideoFrameIter() | 
|  | ] | 
|  |  | 
|  | start_histogram = histograms[1][1] | 
|  | final_histogram = histograms[-1][1] | 
|  | total_distance = start_histogram.Distance(final_histogram) | 
|  |  | 
|  | def FrameProgress(histogram): | 
|  | if total_distance == 0: | 
|  | if histogram.Distance(final_histogram) == 0: | 
|  | return 1.0 | 
|  | else: | 
|  | return 0.0 | 
|  | return 1 - histogram.Distance(final_histogram) / total_distance | 
|  |  | 
|  | return [(time, FrameProgress(hist)) for time, hist in histograms] | 
|  |  | 
|  |  | 
|  | def _ComputeSpeedIndex(completeness_record): | 
|  | """Computes the speed-index from a completeness record. | 
|  |  | 
|  | Args: | 
|  | completeness_record: list(CompletenessPoint) | 
|  |  | 
|  | Returns: | 
|  | Speed-index value. | 
|  | """ | 
|  | speed_index = 0.0 | 
|  | last_time = completeness_record[0][0] | 
|  | last_completness = completeness_record[0][1] | 
|  | for time, completeness in completeness_record: | 
|  | if time < last_time: | 
|  | raise ValueError('Completeness record must be sorted by timestamps.') | 
|  | elapsed = time - last_time | 
|  | speed_index += elapsed * (1.0 - last_completness) | 
|  | last_time = time | 
|  | last_completness = completeness | 
|  | return speed_index | 
|  |  | 
|  |  | 
|  | def ExtractCommonMetricsFromRepeatDirectory(repeat_dir, trace): | 
|  | """Extracts all the metrics from traces and video of a sandwich run repeat | 
|  | directory. | 
|  |  | 
|  | Args: | 
|  | repeat_dir: Path of the repeat directory within a run directory. | 
|  | trace: preloaded LoadingTrace in |repeat_dir| | 
|  |  | 
|  | Contract: | 
|  | trace == LoadingTrace.FromJsonFile( | 
|  | os.path.join(repeat_dir, sandwich_runner.TRACE_FILENAME)) | 
|  |  | 
|  | Returns: | 
|  | Dictionary of extracted metrics. | 
|  | """ | 
|  | run_metrics = { | 
|  | 'chromium_commit': trace.metadata['chromium_commit'], | 
|  | 'platform': (trace.metadata['platform']['os'] + '-' + | 
|  | trace.metadata['platform']['product_model']) | 
|  | } | 
|  | run_metrics.update(_ExtractDefaultMetrics(trace)) | 
|  | run_metrics.update(_ExtractMemoryMetrics(trace)) | 
|  | run_metrics['first_meaningful_paint'] = _ExtractTimeToFirstMeaningfulPaint( | 
|  | trace) | 
|  | video_path = os.path.join(repeat_dir, sandwich_runner.VIDEO_FILENAME) | 
|  | if os.path.isfile(video_path): | 
|  | logging.info('processing speed-index video \'%s\'' % video_path) | 
|  | try: | 
|  | completeness_record = _ExtractCompletenessRecordFromVideo(video_path) | 
|  | run_metrics['speed_index'] = _ComputeSpeedIndex(completeness_record) | 
|  | except video.BoundingBoxNotFoundException: | 
|  | # Sometimes the bounding box for the web content area is not present. Skip | 
|  | # calculating Speed Index. | 
|  | run_metrics['speed_index'] = _FAILED_CSV_VALUE | 
|  | else: | 
|  | run_metrics['speed_index'] = _UNAVAILABLE_CSV_VALUE | 
|  | for key, value in trace.metadata['network_emulation'].iteritems(): | 
|  | run_metrics['net_emul.' + key] = value | 
|  | assert set(run_metrics.keys()) == set(COMMON_CSV_COLUMN_NAMES) | 
|  | return run_metrics |